Extract skills from a resume: the Python options, and the code
Published
Extract skills from a resume: the options, and the code
To extract skills from a resume you have three honest choices, and they are not variations on one another. You can match against a skills taxonomy with a rule-based matcher such as SkillNER, you can run a general resume parser library that returns a skills list among its other fields, or you can call a parsing API that hands back a skills array as part of a typed document. This article is what each one actually returns, the code to run it, and the question that decides between them — whether you need the strings a CV contains, or codes from a controlled vocabulary.
We build ResumeJSON, a per-parse resume parsing API, so read this as written by an interested party. Every release date and price below was read from PyPI, GitHub or the vendor's own page as of September 2026, and every one of them is checkable in a minute.
The options, compared
| Option | What it is | What it gives you | State as of September 2026 |
|---|---|---|---|
| SkillNER | Rule-based matcher over a downloaded skills knowledge base | Skill spans found in free text, with taxonomy ids | skillner 1.0.3 on PyPI, uploaded 7 November 2021; last push to GitHub 28 January 2024; 213 stars |
spaCy PhraseMatcher | Your own dictionary, matched over tokenised text | Exactly the terms in your list | spaCy 3.8.16, 24 August 2026, Python >= 3.9, < 3.15 — actively maintained |
| pyresparser | General resume parser, skills among its fields | A dict with a skills key, plus name, email, degree | 1.0.6, uploaded 15 December 2019, declares Python 3.3 – 3.7; 960 stars |
| Textkernel Skills Intelligence | Enterprise parser with skills normalization as an add-on | Normalized and classified skills against a taxonomy | Sold on quote or credits — Professional starts at $99/month |
| A parsing API | One HTTP call, whole document | skills as an array, beside work, education and the rest | This service is one of these |
The row that decides it is the last column of your own requirements, not this table. If a recruiter is going to read the skills, the strings in the CV are enough. If a matching algorithm is going to compare two candidates, you need a vocabulary, and that is a different purchase.
Extracting skills is two problems, not one
Almost every "extract skills from resume" tutorial solves the first half and quietly leaves the second.
- Finding the candidate strings. A CV says
Python, Django, PostgreSQLunder aSkillsheading, and also says "migrated a Postgres cluster" three paragraphs earlier. Both are evidence. A section-only reader misses the second; a whole-document reader picks up half the prose with it. - Deciding what counts, and what it is called. Is
Postgresthe same skill asPostgreSQL? Isleadershipa skill or a filler word? IsMS Officeworth a row? This is the part a taxonomy answers and a regex does not, and it is the part that decides whether your candidate matching is trustworthy.
A dictionary approach solves (2) by fiat: whatever is in your list is a skill. That is a real answer, and for a narrow domain — twenty languages and fifteen frameworks — it is often the right one.
SkillNER: a taxonomy matcher
SkillNER is the package most people find first, and it is a genuine rule-based matcher rather than a wrapper around a model. It works against a skills knowledge base you download separately:
pip install skillner -U
skillner-download ESCO_ENThe package on PyPI is at 1.0.3, uploaded 7 November 2021, and its GitHub repository was last pushed on 28 January 2024 with 213 stars. It has no release in nearly five years, so treat the install as a fixed artefact rather than a maintained dependency: pin it, vendor the knowledge base, and expect to own the compatibility with whatever spaCy version you run beside it.
What you get back is spans with taxonomy ids, which is exactly the thing a hand-rolled dictionary cannot give you. If your goal is to compare two candidates on the same axis, that identifier is the whole point.
spaCy PhraseMatcher: your own dictionary
If your skill vocabulary is small and you know it, the matcher in spaCy is about fifteen lines and has no abandoned dependency in it. spaCy is at 3.8.16, uploaded 24 August 2026, declaring Python >= 3.9, < 3.15 — the only actively maintained library in this article's first three rows.
import spacy
from spacy.matcher import PhraseMatcher
nlp = spacy.load("en_core_web_sm")
SKILLS = ["python", "django", "postgresql", "kubernetes", "terraform"]
matcher = PhraseMatcher(nlp.vocab, attr="LOWER")
matcher.add("SKILL", [nlp.make_doc(s) for s in SKILLS])
doc = nlp(resume_text)
found = sorted({doc[start:end].text.lower() for _, start, end in matcher(doc)})
print(found)That is genuinely production-viable for a narrow domain. What it will never do is surprise you with a skill you did not think to list — which is a feature when you are filtering for five things, and a defect when you are building a candidate profile.
pyresparser: skills as one field of a parse
pyresparser returns a dict with a skills key alongside name, email, degree and companies. It is the most-linked option in this space, and it is also the oldest: 1.0.6, uploaded 15 December 2019, declaring Python 3.3 – 3.7 in its own classifiers, with its GitHub last pushed 13 September 2023. It has 960 stars, so plenty of people shipped it — on Python versions that are themselves end of life.
Its skills list comes from a CSV shipped inside the package. That is worth knowing before you adopt it: you are not getting a maintained taxonomy, you are getting somebody's 2019 spreadsheet, and extending it means editing a file inside site-packages or forking. Our fuller write-up of that family is Python resume parser: the libraries that work, and the code.
The enterprise end: normalized skills
At the other end of the market, skills are sold as a normalization product rather than as an extraction one. Textkernel lists Skills Normalization as an add-on to its Parser, and describes Skills Intelligence as extracting skills from documents and returning them normalized and classified. Its own FAQ, on textkernel.com, says the output can be normalized against several standards:
Yes. With Textkernel's Parser you can normalize results according to various standards, including O*NET, ISCO, and Textkernel's real-world professions and skills taxonomies.
Their plans, read from the Textkernel pricing page in September 2026, are a free trial with 500 credits, a Professional plan "Starting at $99 /Month" with monthly plans from 500 – 25,000 credits, and an Enterprise plan by quote; a first purchase can take an Accelerator plan of 5,000 credits for $200. We cover the credit arithmetic in Textkernel pricing, explained.
Who should buy this rather than build it: if your product ranks or matches candidates, if you need skills mapped to O\*NET or ISCO codes for reporting, or if you operate across many languages, a normalized taxonomy is not a nice-to-have and no dictionary of yours will substitute for it. Stay with the enterprise vendors.
When a parse gives you the field for free
If what you actually need is "the skills this CV lists", and you also need the work history, the education and the dates — which is the usual case when you are building a job board or an ATS import — then skills extraction is not a separate project. It is one field of a parse.
Here is the whole job against ResumeJSON, which returns typed JSON in about two seconds:
import requests
resp = requests.post(
"https://resumejson-resume-cv-parser-api.p.rapidapi.com/v1/parse",
headers={
"x-rapidapi-key": "YOUR_KEY",
"x-rapidapi-host": "resumejson-resume-cv-parser-api.p.rapidapi.com",
"content-type": "application/json",
},
json={"text": text}, # or send the file — base64 or multipart
timeout=30,
)
resume = resp.json()["resume"]
print(resume["skills"]) # -> ["Python", "Django", "PostgreSQL", ...]skills is an array of strings, beside basics, work, education, certifications, languages and total_years_experience. There are no model downloads, no spaCy pin and no knowledge base to vendor. Scanned and photographed CVs go down the same endpoint. Uploads are accepted to 20 MB and 60,000 characters of text; past either you get a 413 naming the limit rather than a silent truncation.
| SkillNER or your own dictionary | ResumeJSON | |
|---|---|---|
| Setup | pip, a knowledge base, a spaCy pin | One HTTP call |
| Skills you did not list | Found, if the taxonomy has them | Read from the document |
| Taxonomy ids | Yes — that is the point | No — strings as written |
| The rest of the CV | A separate problem | Same response |
| Cost at 100 CVs/month | Your time | $0 — the free rung is a hard cap |
| Cost at 1,000 CVs/month | Your time | $29/month, or $0.05 a parse pay-per-use |
| Runs offline | Yes | No |
The honest gap is the third row. We return the skills a CV states, in the words it states them in — Postgres stays Postgres. We do not map them onto ESCO, O\*NET or ISCO codes. If your matching depends on two candidates who wrote it differently landing on the same identifier, a taxonomy product is what you want, and that is either SkillNER plus its knowledge base or one of the enterprise vendors above.
How to choose, in one pass
- A fixed, narrow list of skills you already know → spaCy
PhraseMatcher, fifteen lines, no abandoned dependency. - Open-ended skills with taxonomy ids, and engineering time to own it → SkillNER, pinned, with the knowledge base vendored.
- **Skills normalized to O\NET or ISCO, at scale, in many languages* → an enterprise parser; budget for a sales conversation.
- Skills plus the rest of the candidate record, shipping this week → a parsing API.
You can see the output shape before you decide anything: the free browser parser runs a real CV through the same endpoint with no signup, and the field reference is generated from the API's own schema, so it cannot drift from what you receive. If you are mapping the result onto a standard document shape, JSON Resume schema: a complete example has the field-by-field table; if you are still weighing build against buy, open source resume parser: what works, and when to buy instead is the broader version of this comparison.
Whichever route you take, decide it on the second problem rather than the first. Finding the strings is a weekend. Agreeing on what they are called is the part that is still there in a year.