Build a Resume Screener in Python: A Mini Project That Shows Recruiters You Can Solve Real Problems
Picture a recruiter with two hundred resumes and one open position. Somewhere in that pile are three or four people who could do the job. Finding them is slow, repetitive and surprisingly hard, which is exactly why companies build tools to help. Now picture your own resume in that pile, with the same "Library Management System" and "Student Portal" projects that fifty other freshers list.
A project that solves a real problem, is built step by step, handles bad input and has tests behind it looks very different. That is what this guide helps you build: a resume screener in Python that reads a job description and a folder of resumes, finds the skills in each one, scores every candidate out of 100, ranks them and explains every score. It uses only Python's standard library, so there is nothing to install, and the finished code is about 200 lines.
This is not a toy that prints "Hello". You will practise the things interviewers ask about: reading files, regular expressions, functions and modules, command-line arguments, CSV output, edge cases, unit tests, debugging and thinking honestly about the limits of your own tool. Every code block and every output in this guide comes from running the actual project.
How to use it: type the code yourself instead of copying it. Each step ends with something you can run and a checkpoint to confirm it works. If you get stuck, the complete project is also provided as a ZIP file with this article.
How the Resume Screener Works
Figure: The six stages every resume goes through
What This Guide Covers
• What you will build, and an honest note on where such tools should and should not be used
• Fourteen steps from an empty folder to a tested, working command-line tool
• A real bug that every beginner hits (why "java" matches "javascript"), and how to fix it
• Unit tests, edge cases, common errors and how to debug them
• Ideas to extend the project, plus its honest limitations
• How to publish it on GitHub, describe it on your resume and explain it in an interview
What You Will Build
By the end, one command will read a job description and five resumes and print a ranked table like this:
Output:
Required skills : git, python, sql
Nice to have : communication, flask, html
Rank Candidate Score Missing required skills
--------------------------------------------------------------
1 Sneha Kulkarni 100.0 none
2 Aarav Sharma 93.3 none
3 Meera Iyer 56.7 python
4 Rohan Das 56.7 git
5 Kiran Patil 20.0 git, sql
Every candidate gets a score, and you can see exactly which required skills are missing. There is no mystery and no black box. That explainability is one of the best things about this design, and it is something you can talk about confidently in an interview.
Before you start: an honest boundary
This is a learning project. Real hiring decisions should always involve people. Keyword screening can miss good candidates and can reflect bias, as the limitations section explains. All resumes used here are completely fictional. Please do not use this tool to reject real applicants.
What You Need
• Python 3.8 or above. The outputs here were produced with Python 3.12. Check your version by typing python --version in a terminal (on Windows you may need py --version).
• A text editor. VS Code, PyCharm, or even Notepad works.
• A terminal (Command Prompt, PowerShell or any Linux or Mac terminal).
• No extra libraries. Everything uses modules that ship with Python: re, pathlib, csv, argparse and unittest.
Step 1: Create the Project Folder
Good projects start with a clean structure. Create a folder called resume_screener with three subfolders: data, data/resumes and tests. In a terminal:
mkdir resume_screener
cd resume_screener
mkdir data
mkdir data/resumes
mkdir tests
By the end of the guide, your folder will look like this:
The Final Project Structure
Figure: Each file has one clear job
Why split into several files? One giant script works, but small modules are easier to read, test and explain. Each file does one thing, which is a habit recruiters like to see.
A small tip
We named the file resume_parser.py and not parser.py on purpose. Avoid naming your files after Python's own modules, because a name clash can cause confusing errors that are hard to trace.
Checkpoint: your folder exists and contains the empty data, data/resumes and tests folders.
Step 2: Create the Sample Data
A screener needs something to screen. Create a job description and five fictional resumes as plain text files. Each resume is designed to teach you something, so read the notes after the files.
data/job_description.txt
Junior Python Developer
We are looking for a fresher or junior developer to join our backend team.
Required skills: Python, SQL, Git
Nice to have: Flask, HTML, communication
Responsibilities: build small REST APIs, write clean code, work with a MySQL database and collaborate with the team.
The two lines that start with "Required skills:" and "Nice to have:" are what our program will read. Keeping a simple, predictable format is a deliberate design choice that makes the parser easy to write.
data/resumes/sneha_kulkarni.txt
Sneha Kulkarni
Email: sneha.kulkarni@example.com | Phone: +91-9876500004
Objective
Backend-focused fresher with good communication and teamwork.
Skills
Python, Django, Flask, SQL (PostgreSQL), Git and GitHub, HTML, CSS
Projects
Expense Tracker API - built with Flask and PostgreSQL, version controlled on GitHub.
Internship
Backend Intern (2 months) - fixed bugs and wrote small API endpoints.
Education
B.Tech Information Technology
data/resumes/aarav_sharma.txt
Aarav Sharma
Email: aarav.sharma@example.com | Phone: +91 9876500001
Objective
Fresher seeking a junior Python developer role.
Skills
Python, SQL (MySQL), Git, Flask, HTML
Projects
Library Management API - built a small REST API in Flask with a MySQL database and tracked all work using Git.
Internship
Python Intern (3 months) - wrote scripts to clean data and generate weekly reports.
Education
B.E. Computer Engineering
data/resumes/meera_iyer.txt
Meera Iyer
Email: meera.iyer@example.com | Phone: 9876500002
Objective
Fresher interested in front-end and Java development.
Skills
Java, JavaScript, HTML, CSS, SQL, GitHub
Projects
Student Portal - a web app built with HTML, CSS and JavaScript, with Java and SQL on the back end.
Education
B.Sc. Computer Science
data/resumes/rohan_das.txt
Rohan Das
Email: rohan.das@example.com | Phone: 9876500003
Objective
Aspiring data analyst with strong communication skills.
Skills
Python, pandas, Excel, SQL
Projects
Sales Data Analysis - cleaned a sales dataset with pandas and presented findings to classmates.
Education
B.Com with a data analytics certificate
data/resumes/kiran_patil.txt
Kiran Patil
Email: kiran.patil@example.com | Phone: 9876500005
Skills
Python Python Python Python Python Java Java Java
Education
B.Sc. Information Technology
Why these five? Each one tests a different situation:
• Sneha has every required and nice-to-have skill, plus a project and an internship. She should rank first.
• Aarav has all the required skills but misses "communication". He should rank second.
• Meera knows Java and front-end skills, and lists GitHub but not Python. She tests the alias idea (GitHub counts as Git).
• Rohan has data skills and mentions Python and SQL, but no Git.
• Kiran types "Python" five times and "Java" three times. He tests whether repeating a keyword unfairly boosts a score.
Checkpoint: you have one job description and five resume files, all saved as plain text with the UTF-8 encoding (the default in most editors).
Step 3: Build the Skill Dictionary
Two people can write the same skill in different ways: "SQL", "MySQL" and "PostgreSQL" all mean database skills, and "Git" and "GitHub" both mean version control. If your program only looks for the exact word "sql", it will miss Aarav, whose resume says "MySQL". The fix is a dictionary of aliases. Create skills.py:
"""Skill dictionary: each skill maps to the words that mean the same thing."""
SKILL_ALIASES = {
"python": ["python"],
"java": ["java"],
"javascript": ["javascript", "js"],
"sql": ["sql", "mysql", "postgresql", "sqlite"],
"git": ["git", "github"],
"flask": ["flask"],
"django": ["django"],
"html": ["html", "html5"],
"css": ["css", "css3"],
"pandas": ["pandas"],
"excel": ["excel"],
"communication": ["communication", "communicate"],
}
Each key is the skill we care about, and each list holds the different ways people write it. When any alias is found, the skill counts as found. Adding a new skill later takes one line.
Checkpoint: skills.py exists and has no syntax errors. You can test it by typing python skills.py, which should run silently.
Step 4: Read and Clean the Text
Resumes are messy: different capitalisation, extra spaces, line breaks in odd places. Before searching, we normalise the text. Start resume_parser.py with two small functions:
import re
from pathlib import Path
def read_text(path):
"""Read a text file safely, ignoring characters that cannot be decoded."""
return Path(path).read_text(encoding="utf-8", errors="ignore")
def clean_text(text):
"""Lowercase the text and collapse all whitespace into single spaces."""
return re.sub(r"\s+", " ", text.lower()).strip()
Let us see what each one does. read_text opens a file as UTF-8 and ignores any character it cannot decode, so one odd symbol will not crash the program. clean_text lowercases everything and squeezes all whitespace into single spaces. The regular expression \s+ means "one or more whitespace characters" (spaces, tabs and new lines).
For example, the messy text " Python,\n\n SQL (MySQL) " becomes a tidy single line:
Output:
python, sql (mysql)
Checkpoint: you can call clean_text on a messy string and get lowercase text with single spaces.
Step 5: Find Skills (and Meet a Classic Bug)
The obvious way to check whether a resume mentions Java is this: if "java" in text. It looks fine, and it is wrong. Watch what happens with a resume that only mentions JavaScript:
text = "Experienced in JavaScript and React"
print("java" in text.lower()) # naive check
Output:
True
True! The word "java" is hiding inside "javascript", so a JavaScript developer would be wrongly counted as knowing Java. This is a real bug that appears in many beginner projects, and spotting it shows good engineering instincts.
The fix: match whole words only
We need to match a term only when it is not part of a longer word. A regular expression can do this with two small checks called lookarounds:
def contains_term(text, term):
"""True only if `term` appears as a whole word (so 'java' will not match 'javascript')."""
pattern = r"(?<![a-z0-9])" + re.escape(term.lower()) + r"(?![a-z0-9])"
return re.search(pattern, text) is not None
In plain language: (?<![a-z0-9]) means "the character before must not be a letter or digit", and (?![a-z0-9]) means "the character after must not be a letter or digit". The function re.escape makes sure special characters in a term are treated as plain text. Now test it:
from resume_parser import contains_term, clean_text
text = "Experienced in JavaScript and React"
print(contains_term(clean_text(text), "java")) # whole-word check
print(contains_term(clean_text(text), "javascript"))
Output:
False
True
Java is no longer found inside JavaScript, while "javascript" itself is still found. Bug fixed.
Putting it together: find_skills
def find_skills(text, skill_aliases):
"""Return the set of skills found in the text."""
cleaned = clean_text(text)
found = set()
for skill, aliases in skill_aliases.items():
if any(contains_term(cleaned, alias) for alias in aliases):
found.add(skill)
return found
This loops through every skill, checks each alias with contains_term, and collects the skills that were found into a set. We use a set on purpose: it holds each skill only once, so repeating a word ten times cannot inflate anything.
Checkpoint: run the small test below from your project folder. For Aarav's resume, you should see the skills flask, git, html, python and sql.
Output:
['flask', 'git', 'html', 'python', 'sql']
Step 6: Extract Contact Details and Signals
A recruiter also wants a name, an email and a phone number, and a quick sense of whether the candidate has projects or an internship. Add the following to resume_parser.py. First, two regular expressions at the top of the file (below the imports):
EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w-]+\.[\w-]+(?:\.[\w-]+)*")
PHONE_PATTERN = re.compile(r"(?:\+91[\s-]?)?[6-9]\d{9}")
The email pattern says: some letters, digits, dots, plus signs or hyphens, then an @, then a domain with at least one dot. The phone pattern accepts an optional +91 prefix followed by a ten-digit Indian mobile number starting with 6, 7, 8 or 9. Neither pattern is perfect (no regular expression for email ever is), but they are fine for a learning project.
Now the helper functions:
def extract_name(text):
"""Assume the first non-empty line of a resume is the candidate's name."""
for line in text.splitlines():
if line.strip():
return line.strip()
return "Unknown"
def extract_email(text):
match = EMAIL_PATTERN.search(text)
return match.group(0) if match else ""
def extract_phone(text):
match = PHONE_PATTERN.search(text)
return match.group(0) if match else ""
def has_internship(text):
return contains_term(clean_text(text), "internship") or contains_term(clean_text(text), "intern")
def has_projects(text):
cleaned = clean_text(text)
return contains_term(cleaned, "projects") or contains_term(cleaned, "project")
Notice the small assumption in extract_name: the first non-empty line is treated as the name. That works for our sample resumes, and it is exactly the kind of assumption you should write down, because real resumes will break it.
Run a quick check on Aarav's resume:
Output:
Aarav Sharma | aarav.sharma@example.com | +91 9876500001
projects: True | internship: True
Checkpoint: your resume_parser.py now has two patterns and nine functions. If you prefer to see the complete file, it is included in the ZIP.
Step 7: Read the Job Description
The screener must know which skills the job requires and which are only nice to have. Create scorer.py and add this function:
from resume_parser import find_skills
def parse_job_description(text, skill_aliases):
"""Read the 'Required skills:' and 'Nice to have:' lines of a job description."""
required, nice = set(), set()
for line in text.splitlines():
lower = line.lower().strip()
if lower.startswith("required skills:"):
required = find_skills(line.split(":", 1)[1], skill_aliases)
elif lower.startswith("nice to have:"):
nice = find_skills(line.split(":", 1)[1], skill_aliases)
return required, nice
It reads the job description line by line. When a line starts with "required skills:", it takes the text after the colon and runs find_skills on it. The same happens for "nice to have:". Reusing find_skills here is neat: the job description gets the same alias treatment as the resumes.
Check the result for our job description:
Output:
required: ['git', 'python', 'sql']
nice : ['communication', 'flask', 'html']
Checkpoint: you should see three required skills (git, python, sql) and three nice-to-have skills (communication, flask, html).
Step 8: Score Each Resume
Now the heart of the project. We turn matched skills into a number. A fair, explainable approach is to give each part of the resume a share of 100 points:
How the 100 Points Are Split
Add these constants and functions to scorer.py:
# These weights are our own design choices. Change them to fit your job.
REQUIRED_WEIGHT = 60
NICE_WEIGHT = 20
PROJECT_POINTS = 10
INTERNSHIP_POINTS = 10
def ratio(found, wanted):
"""Share of the wanted skills that were found (0.0 to 1.0)."""
if not wanted:
return 0.0
return len(found & wanted) / len(wanted)
def score_resume(found_skills, required, nice, has_projects, has_internship):
score = ratio(found_skills, required) * REQUIRED_WEIGHT
score += ratio(found_skills, nice) * NICE_WEIGHT
if has_projects:
score += PROJECT_POINTS
if has_internship:
score += INTERNSHIP_POINTS
return {
"score": round(score, 1),
"matched_required": sorted(found_skills & required),
"missing_required": sorted(required - found_skills),
"matched_nice": sorted(found_skills & nice),
}
The ratio function answers: "What share of the wanted skills did we find?" The expression found & wanted is a set intersection, meaning "skills that are in both sets". Dividing by the number of wanted skills gives a value from 0 to 1.
Walk through one score by hand
Take Meera. The job requires Python, SQL and Git. Meera has SQL and Git (through GitHub) but not Python.
• Required skills: 2 of 3 found, so 2 / 3 x 60 = 40.0 points
• Nice-to-have skills: only HTML found, 1 of 3, so 1 / 3 x 20 = 6.7 points
• Projects: she has a Projects section, so +10 points
• Internship: none, so +0 points
• Total: 40.0 + 6.7 + 10 = 56.7
Now let the program calculate it and compare:
Output:
{'score': 56.7, 'matched_required': ['git', 'sql'], 'missing_required': ['python'], 'matched_nice': ['html']}
The program agrees with the hand calculation: 56.7, with Python listed as the missing required skill. Whenever you write scoring code, check at least one result by hand like this. It is the fastest way to catch logic errors.
Checkpoint: score_resume returns a dictionary with the score, the matched required skills, the missing required skills and the matched nice-to-have skills.
Step 9: Show and Save the Results
A score nobody can read is useless. Create report.py to print a clean table and to save every result as a CSV file that opens in Excel:
"""Showing and saving the results."""
import csv
def print_table(results, required, nice):
print("Required skills :", ", ".join(sorted(required)))
print("Nice to have :", ", ".join(sorted(nice)))
print()
print(f"{'Rank':<5}{'Candidate':<18}{'Score':<8}{'Missing required skills'}")
print("-" * 62)
for rank, r in enumerate(results, start=1):
missing = ", ".join(r["missing_required"]) or "none"
print(f"{rank:<5}{r['name']:<18}{r['score']:<8}{missing}")
def save_csv(results, path):
fields = ["name", "email", "score", "matched_required", "missing_required", "matched_nice", "file"]
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for r in results:
row = dict(r)
for key in ("matched_required", "missing_required", "matched_nice"):
row[key] = "; ".join(row[key])
writer.writerow({k: row[k] for k in fields})
Some details worth understanding:
• In the f-string, {r["name"]:<18} means "left-align this text in 18 characters", which keeps the columns tidy.
• enumerate(results, start=1) gives rank numbers starting at 1 instead of 0.
• The CSV writer joins skill lists with semicolons so each skill list stays inside one cell.
• Opening the file with newline="" is the correct way to write CSV files in Python and avoids blank lines on Windows.
Step 10: Build the Command-Line Tool
Finally, tie everything together in main.py. We use argparse so users can run the tool with options instead of editing code:
"""Command-line entry point: python main.py --jd ... --resumes ..."""
import argparse
from pathlib import Path
from skills import SKILL_ALIASES
from resume_parser import (read_text, find_skills, extract_name, extract_email,
has_internship, has_projects)
from scorer import parse_job_description, score_resume
from report import print_table, save_csv
def main():
ap = argparse.ArgumentParser(description="A simple, explainable resume screener")
ap.add_argument("--jd", required=True, help="path to the job description text file")
ap.add_argument("--resumes", required=True, help="folder containing .txt resumes")
ap.add_argument("--top", type=int, default=5, help="how many candidates to show")
ap.add_argument("--csv", help="optional path to save all results as CSV")
args = ap.parse_args()
required, nice = parse_job_description(read_text(args.jd), SKILL_ALIASES)
if not required:
print("No required skills found. Add a line like 'Required skills: Python, SQL'.")
return
results = []
for path in sorted(Path(args.resumes).glob("*.txt")):
text = read_text(path)
found = find_skills(text, SKILL_ALIASES)
result = score_resume(found, required, nice, has_projects(text), has_internship(text))
result.update({"name": extract_name(text), "email": extract_email(text), "file": path.name})
results.append(result)
if not results:
print("No .txt resumes found in", args.resumes)
return
results.sort(key=lambda r: (-r["score"], r["name"])) # highest score first, ties by name
print_table(results[:args.top], required, nice)
if args.csv:
save_csv(results, args.csv)
print(f"\nSaved {len(results)} results to {args.csv}")
if __name__ == "__main__":
main()
Read it slowly, because several important ideas are packed in here:
• argparse creates the --jd, --resumes, --top and --csv options and generates a --help message for free.
• Path(...).glob("*.txt")** finds every text file in the resumes folder, and sorted() makes the order predictabl
• result.update(...) adds name, email and file name to each score dictionary.
• The sort key (-r["score"], r["name"]) sorts by highest score first, and breaks ties alphabetically by name. Without a tie-break, equal scores could appear in a random order.
• Two early exits protect against a job description with no required skills and a folder with no resumes. Handling bad input politely is a mark of careful work.
• if __name__ == "__main__" means main() runs only when the file is executed directly, not when it is imported by a test.
Step 11: Run It
From inside the resume_screener folder, run:
python main.py --jd data/job_description.txt --resumes data/resumes --top 5 --csv results.csv
Output:
Required skills : git, python, sql
Nice to have : communication, flask, html
Rank Candidate Score Missing required skills
--------------------------------------------------------------
1 Sneha Kulkarni 100.0 none
2 Aarav Sharma 93.3 none
3 Meera Iyer 56.7 python
4 Rohan Das 56.7 git
5 Kiran Patil 20.0 git, sql
Saved 5 results to results.csv
Here is how to read the result like a recruiter would:
• Sneha (100.0) matches everything, so she is first.
• Aarav (93.3) has all required skills, but misses the nice-to-have "communication".
• Meera and Rohan (both 56.7) tie. Meera misses Python, while Rohan misses Git. Because we sorted ties by name, Meera comes first. A tie-break rule means the same input always gives the same output.
• Kiran (20.0) typed Python five times and still scored low. Our scoring counts whether a skill is present, not how many times it is repeated, so keyword stuffing does not work.
The CSV file contains the same results in a spreadsheet-friendly format. Its first lines look like this:
Output:
name,email,score,matched_required,missing_required,matched_nice,file
Sneha Kulkarni,sneha.kulkarni@example.com,100.0,git; python; sql,,communication; flask; html,sneha_kulkarni.txt
Aarav Sharma,aarav.sharma@example.com,93.3,git; python; sql,,flask; html,aarav_sharma.txt
You can also show fewer candidates with the --top option. For example, with --top 3:
Output:
Required skills : git, python, sql
Nice to have : communication, flask, html
Rank Candidate Score Missing required skills
--------------------------------------------------------------
1 Sneha Kulkarni 100.0 none
2 Aarav Sharma 93.3 none
3 Meera Iyer 56.7 python
Checkpoint: you should see the same ranking. If your numbers differ, compare your files with the ZIP, paying attention to spelling and indentation.
Step 12: Handle the Edge Cases
A real tool must behave sensibly when something is wrong. We have already added two guard clauses. Let us check them. First, a resumes folder that is empty:
python main.py --jd data/job_description.txt --resumes empty_folder
Output:
No .txt resumes found in empty_folder
Second, a job description that has no "Required skills:" line:
python main.py --jd bad_jd.txt --resumes data/resumes
Output:
No required skills found. Add a line like 'Required skills: Python, SQL'.
Instead of crashing with a confusing error, the program tells the user what to fix. Interviewers love hearing, "I tested what happens with empty and invalid input."
Step 13: Write Unit Tests
Running the program once and seeing a sensible answer is not the same as knowing it works. Tests protect you from breaking things later. Create tests/test_screener.py:
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from skills import SKILL_ALIASES
from resume_parser import contains_term, find_skills, extract_email, extract_phone, clean_text
from scorer import parse_job_description, score_resume
class TestMatching(unittest.TestCase):
def test_java_does_not_match_javascript(self):
self.assertFalse(contains_term(clean_text("Experienced in JavaScript"), "java"))
self.assertTrue(contains_term(clean_text("Experienced in Java and SQL"), "java"))
def test_github_counts_as_git(self):
self.assertIn("git", find_skills("Projects hosted on GitHub", SKILL_ALIASES))
def test_mysql_counts_as_sql(self):
self.assertIn("sql", find_skills("Used MySQL for storage", SKILL_ALIASES))
def test_repeating_a_keyword_does_not_add_points(self):
once = find_skills("Python", SKILL_ALIASES)
many = find_skills("Python Python Python Python", SKILL_ALIASES)
self.assertEqual(once, many)
class TestExtraction(unittest.TestCase):
def test_email_and_phone(self):
text = "Email: a.b@example.com | Phone: +91 9876500001"
self.assertEqual(extract_email(text), "a.b@example.com")
self.assertEqual(extract_phone(text), "+91 9876500001")
def test_missing_contact_details(self):
self.assertEqual(extract_email("no contact here"), "")
class TestScoring(unittest.TestCase):
def setUp(self):
self.required = {"python", "sql", "git"}
self.nice = {"flask", "html", "communication"}
def test_perfect_resume_scores_100(self):
found = self.required | self.nice
self.assertEqual(score_resume(found, self.required, self.nice, True, True)["score"], 100.0)
def test_empty_resume_scores_zero(self):
self.assertEqual(score_resume(set(), self.required, self.nice, False, False)["score"], 0.0)
def test_missing_skills_are_reported(self):
result = score_resume({"python"}, self.required, self.nice, False, False)
self.assertEqual(result["missing_required"], ["git", "sql"])
def test_job_description_parsing(self):
jd = "Required skills: Python, SQL\nNice to have: Flask"
required, nice = parse_job_description(jd, SKILL_ALIASES)
self.assertEqual(required, {"python", "sql"})
self.assertEqual(nice, {"flask"})
if __name__ == "__main__":
unittest.main()
Python's built-in unittest module finds every method that starts with test_. A few of these tests deserve a closer look:
• test_java_does_not_match_javascript locks in the bug fix from Step 5, so it can never silently return.
• test_repeating_a_keyword_does_not_add_points proves that keyword stuffing is ignored.
• test_perfect_resume_scores_100 and test_empty_resume_scores_zero check the two extremes of the scoring.
• sys.path.insert(...) at the top lets the tests import our modules from the parent folder.
Run all the tests from the project folder:
python -m unittest discover -s tests -v
Output:
test_email_and_phone ... ok
test_missing_contact_details ... ok
test_github_counts_as_git ... ok
test_java_does_not_match_javascript ... ok
test_mysql_counts_as_sql ... ok
test_repeating_a_keyword_does_not_add_points ... ok
test_empty_resume_scores_zero ... ok
test_job_description_parsing ... ok
test_missing_skills_are_reported ... ok
test_perfect_resume_scores_100 ... ok
----------------------------------------------------------------------
Ran 10 tests in 0.002s
OK
Ten tests, all passing. Notice how small and fast they are. A project with tests tells a recruiter that you care about correctness, which is a much stronger signal than a project without them.
Step 14: Common Errors and How to Fix Them
• ModuleNotFoundError: No module named "resume_parser". You are running the program from the wrong folder. Use cd to go inside resume_screener first.
• FileNotFoundError. A path in your command is wrong. Check that data/job_description.txt and data/resumes exist relative to where you are running the command.
• "No required skills found". Your job description line must start exactly with "Required skills:" followed by skills the dictionary knows. Add any missing skill to skills.py.
• All scores are 0. Your resumes may not be saved as .txt files, or your skill names may not match the aliases. Print find_skills(text, SKILL_ALIASES) for one resume to see what was found.
• A skill is not detected. Add the missing spelling to its alias list, such as "postgres" under sql.
• An error mentioning "python" is not recognised on Windows. Try py instead of python, or reinstall Python and tick "Add Python to PATH".
• Odd characters in a resume. We already handle this with errors="ignore". If you see missing words, resave the file as UTF-8.
A habit worth building: when something breaks, print the intermediate value. Most bugs in a project like this are visible the moment you print what each function returned.
The Honest Limitations of This Screener
A strong project is one whose author understands its weaknesses. Here are the important ones.
1. Keywords have no sense of context
Our program cannot tell the difference between "I am experienced in Python" and "I have no experience with Python". Look at what it does with the second sentence:
from skills import SKILL_ALIASES
from resume_parser import find_skills
print(sorted(find_skills("I have no experience with Python", SKILL_ALIASES)))
Output:
['python']
It found Python, which is wrong in spirit. Fixing this takes real language understanding, not just keyword matching.
2. It can miss good candidates
A strong candidate who wrote "built web services" instead of "Flask" would score lower. Skills described in different words are invisible to our dictionary.
3. Automated screening can be biased
Any tool that filters people can reflect the biases of the person who designed it. For example, a skill list that favours certain backgrounds, or a rule that penalises resume formats, can unfairly exclude people. Our weights (60, 20, 10, 10) are our own choices and not an industry standard.
4. It reads plain text only
Real resumes arrive as PDFs and Word documents, so a real screener would need a text-extraction step first.
Why this honesty matters
In an interview, saying "my tool is useful, but here is where it fails and here is how I would improve it" sounds far more mature than claiming it is perfect. Recruiters trust candidates who understand limits.
Make It Better: Ideas to Extend the Project
Once the base project works, extending it is where you stand out. Here are ideas from easy to ambitious. These extensions were not built in this guide, so test each one yourself.
• Move the weights into a JSON file so anyone can change them without touching code. (Easy: json module.)
• Add a "why this score" explanation for each candidate in plain sentences. (Easy.)
• Detect years of experience with a regular expression such as the number before the word "years". (Medium.)
• Anonymise resumes by hiding names before scoring, to reduce bias and show you thought about fairness. (Medium.)
• Support PDF and Word resumes using a library such as pypdf or python-docx. (Medium: needs pip install and testing.)
• Handle negation so "no experience with Python" does not count as a match. (Hard.)
• Try similarity scoring with TF-IDF using scikit-learn, and compare it with your keyword approach. (Hard.)
• Wrap it in a simple web page using Flask or Streamlit so a non-programmer can use it. (Hard.)
How to Publish It on GitHub
A project that nobody can see does not help your job search. Follow these steps:
1. Create a repository named resume-screener-python (or a similar clear name).
2. Add a README.md that explains the project in the first few lines. Use the template below.
3. Commit in small steps with clear messages, such as "Add skill dictionary" and "Fix java matching inside javascript". Your commit history tells the story of how you built it.
4. Add a screenshot or pasted output of the ranked table to the README.
5. Remember to mention the limitations. It signals maturity.
6. Keep the sample data fictional and never upload real resumes or personal details.
A README template you can adapt (this one is also in the ZIP):
# Resume Screener (Python)
A small, explainable command-line tool that scores plain-text resumes against a job description.
Built with the Python standard library only. No installs needed.
## Features
- Reads a job description with "Required skills:" and "Nice to have:" lines
- Finds skills using whole-word matching and aliases (for example, MySQL counts as SQL)
- Scores each resume out of 100 and shows which required skills are missing
- Prints a ranked table and can save all results to CSV
- Includes unit tests
## How to run
python main.py --jd data/job_description.txt --resumes data/resumes --top 5 --csv results.csv
## Run the tests
python -m unittest discover -s tests -v
## How scoring works
60 points for required skills, 20 for nice-to-have skills, 10 for projects and 10 for an internship.
The weights are design choices and can be changed in scorer.py.
## Limitations
Keyword matching cannot understand context and can reflect bias. Use this project to learn,
not to reject real candidates. All sample resumes are fictional.
How to Describe It on Your Resume
Be specific and honest. Describe what you built, how and with what, without inventing numbers. Here are two ways to write it:
Resume project entry
Resume Screener (Python): Built a command-line tool that reads a job description and a folder of resumes, matches skills using whole-word regular expressions and aliases, scores each candidate out of 100 with an explanation, ranks them and exports results to CSV. Wrote 10 unit tests covering edge cases and a keyword-matching bug fix. Tools: Python, re, argparse, csv, unittest, Git.
Add the GitHub link right under the project title. Only include a number such as the test count if it is true, and in this project it is, because the test run above shows ten passing tests.
How to Talk About It in an Interview
A 60-second explanation you can adapt
Say it like this
"I built a resume screener in Python to understand how recruiters filter large piles of applications. It reads a job description and resumes, finds the skills using regular expressions, scores each resume out of 100 and explains what is missing. One interesting bug was that my first version matched Java inside JavaScript, so I switched to whole-word matching and wrote a test so it could never come back. I also know its limits: it ignores context and can reflect bias, so I would use it to support a human reviewer, not replace one."
Questions you may be asked
• Why did you use regular expressions with word boundaries? Because a simple "in" check matches inside longer words, such as java inside javascript.
• Why a set for skills? A set stores each skill once, so repeating a keyword cannot raise the score, and set intersections make the scoring code short.
• Why did you choose an explainable score instead of machine learning? Because it is transparent, easy to debug and each decision can be explained to a person. I know machine learning options such as TF-IDF exist, and I listed them as extensions.
• How did you test it? With unittest, including edge cases such as an empty resume, a perfect resume, missing contact details and keyword repetition.
• What would you improve? Handle negation, support PDF resumes, move the weights to a config file and anonymise names to reduce bias.
• What are its risks? It can miss good candidates, misread context and reflect bias in the skill list or weights. It should assist human reviewers.
A 7-Day Plan to Build and Publish It
• Day 1: Set up the folder, write the sample data and create skills.py (Steps 1 to 3).
• Day 2: Build the parser: reading, cleaning, whole-word matching and contact extraction (Steps 4 to 6).
• Day 3: Parse the job description and write the scoring logic, then check a score by hand (Steps 7 and 8).
• Day 4: Build the report, the command-line tool and run the full project (Steps 9 to 11).
• Day 5: Add edge-case handling and unit tests (Steps 12 and 13).
• Day 6: Write the README, add a screenshot, and publish on GitHub.
• Day 7: Add one extension from the list and practise the 60-second explanation aloud.
Where Structured Guidance Can Help
Building projects is the best way to learn, and getting feedback on them is what makes you improve faster. If you would like guided learning, a centre such as upGrad Learning Support Centre, Pune can be a useful next step to explore, especially if you want structured practice in areas such as Python, data science, AI/ML or full stack development, along with project work and interview preparation.
Structured programmes generally combine a planned curriculum, hands-on projects, mentor guidance and interview preparation. Course content, fees, schedules and eligibility can change, so confirm the latest details directly with the centre.
Before joining any programme, ask whether you will build real projects and receive feedback on them, whether mock interviews are included, and whether the curriculum matches what employers ask for today. Be cautious about anyone who guarantees a job, interviews or a salary, because nobody can honestly promise that.
Get Regular Job Updates
Looking for regular IT job updates, fresher opportunities, and career-related updates? Join our WhatsApp group for more job updates and opportunities.
Join our WhatsApp Group:
https://chat.whatsapp.com/KA8HpgjbJ67I5yfQDquCvn
Frequently Asked Questions (FAQs)
Q. Can a fresher build this resume screener project?
Yes. If you know basic Python (variables, loops, functions, lists and dictionaries), you can build it by following the steps. The regular expressions are the hardest part, and this guide explains each one in plain words.
Q. Do I need machine learning or NLP for this project?
No. This version uses simple keyword matching with aliases and is fully explainable. Machine learning approaches such as TF-IDF are good extensions, but they are not needed to build a useful project.
Q. Is this project good enough for a fresher resume?
A tested, documented project that solves a real problem and is published on GitHub is a solid addition. It becomes stronger if you add an extension and can explain its limitations in an interview.
Q. Why did you use only the standard library?
So there is nothing to install and nothing to go wrong during setup. It also lets you learn the fundamentals before depending on external packages.
Q. Can this tool read PDF resumes?
Not in this version, which reads plain text. You can extend it with a library such as pypdf to extract text from PDFs, then pass that text through the same pipeline.
Q. Is it right to use a tool like this to reject candidates?
This project is for learning. Real hiring decisions should involve people, because keyword screening misses context and can reflect bias. Use such tools only to support human review.
Q. How long will the project take?
Around a week at a comfortable pace, as in the 7-day plan, if you type the code yourself and run each checkpoint. Extensions take additional time.
Q. What should I learn next after this project?
Learn more Python (classes, file formats like JSON), Git and GitHub, writing more tests, and then try an extension such as a web interface or PDF support.
Conclusion
You started with an empty folder and finished with a working, tested, explainable tool. Along the way you read files, cleaned messy text, fixed a real matching bug, scored candidates fairly, handled bad input, wrote unit tests and thought honestly about what your tool can and cannot do. That combination, a real problem, clean structure, tests and honest limits, is what turns a practice exercise into a project that recruiters take seriously.
Your next step is simple. Create the folder today, type Steps 1 to 3, and run your first checkpoint. If you want mentor feedback and structured practice along the way, you can also explore what upGrad Learning Support Centre, Pune offers for your chosen path.
And if you want regular job and fresher opportunity updates, use the WhatsApp group link in the "Get Regular Job Updates" section above.


