Build Your Own ATS Resume Scanner
Use TF-IDF and cosine similarity to score resumes against job descriptions.
Introduction
30 Second Summary
You submit a job application, and it vanishes into a black hole. No response, no interview, nothing. Most of the time, a human never even sees your resume. An Applicant Tracking System (ATS) scores and filters it out before a recruiter opens the pile.
In this project, you will build your own ATS resume scanner that uses Natural Language Processing techniques to score your resume against any job description. You will see the same match score, keyword gaps, and formatting issues that a real ATS flags.
What You'll Build
A personal ATS Resume Scanner web app that parses your resume (PDF or DOCX), compares it against a job description using TF-IDF vectorization and cosine similarity, and displays an interactive dashboard with scores, keywords, and improvement suggestions.
By the end of this project, you'll have:
- A Python-based scoring engine that converts resumes and job descriptions into comparable vectors and produces a match score from 0 to 100.
- A resume parser that extracts structured text from PDF and DOCX files and detects standard sections like Experience, Education, and Skills.
- An interactive Streamlit web app with file upload, keyword analysis, section-level scores, and actionable suggestions you can use before every application.
- Secret Mission: a multi-resume comparison mode that lets you upload several versions of your resume and see which one scores highest against the same job description.
Are there any prerequisites?
You need basic Python knowledge (variables, functions, loops). No NLP or machine learning experience is required.
Set Up Your Python Environment
Before building the ATS scanner, you need a working Python environment with the right libraries installed. This step gets your machine ready to run NLP code that parses documents and compares text.
You will verify that Python 3.10+ is installed, create an isolated project space with a virtual environment, and install the four packages that power the scanner.
In this step, get ready to:
- Verify Python 3.10+ is installed on your machine.
- Create a project directory with a virtual environment.
- Install streamlit, scikit-learn, pdfplumber, and python-docx.
Verify your Python installation
This project requires Python 3.10 or higher because Streamlit (the web framework you will use) needs it.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your Python version by running the command for your operating system:
macOS
python3 --version
Windows
python --version
Look at the version number that prints out, then pick the tab below that matches what you see.
✔️ I see version 3.10 or higher
You are all set. Move on to the next substep below.
ⓧ I see an older version
Your system has an older Python that does not meet the 3.10+ requirement. You need to install a newer version.
On macOS, the system Python is often outdated. The recommended path is to install a newer version from the official installer.
- Visit python.org/downloads and download the latest Python 3 installer for your operating system.
- Run the installer and follow the prompts. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then re-run the version check command above.
macOS users with a shadowed system Python
macOS may continue to point python3 at the old system version even after installing a newer one. If you still see the old version after installing, try running the full path to the new binary (e.g. python3.14 --version) or use a version manager like pyenv to activate the correct version.
ⓧ Command not found
Python is not installed on your machine yet. Follow these steps to install it.
- Visit python.org/downloads and download the latest Python 3 installer for your operating system.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal after installation finishes.
- Run the version check command again. You should now see a version number of 3.10 or higher.
Create your project directory and virtual environment
A virtual environment isolates your project's packages from everything else on your machine. This prevents version conflicts between different Python projects.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir ats-scanner
cd ats-scanner
- Confirm the folder exists by running:
ls
You should see the ats-scanner folder listed if you run ls from the Desktop, or an empty output if you are already inside the folder.
- Create a virtual environment inside your project folder by running the command for your operating system:
macOS
python3 -m venv venv
Windows
python -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activate command again.
On macOS, make sure you ran source venv/bin/activate (not just the path without source).
Help me activate my Python virtual environment.
Install the required packages
With your virtual environment active, you can now install the four libraries that power the ATS scanner. Each handles a different job: document parsing, text analysis, and the web interface.
- Install all four packages at once by running:
pip install streamlit scikit-learn pdfplumber python-docx
What are these packages?
- streamlit builds the interactive web dashboard you will use to upload resumes and view scores.
- scikit-learn provides the TF-IDF vectorizer and cosine similarity functions for comparing text.
- pdfplumber extracts text from PDF files.
- python-docx reads text from Word (.docx) files.
The install will download several dependencies. Wait until you see a success message before continuing.
- Verify all packages are importable by running:
python -c "import streamlit; import sklearn; import pdfplumber; import docx; print('All packages ready!')"
You should see All packages ready! printed in your terminal.
Seeing an ImportError?
Make sure (venv) is visible in your prompt. If it is not, re-run the activate command from the previous substep before retrying the install.
If you see an error about sklearn, confirm you installed scikit-learn (the package name on PyPI) and not something else.
Help me fix my package imports.
- Open the ats-scanner folder in VS Code. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the ats-scanner folder, and click Open.
Why open the folder in VS Code?
Opening the project folder (rather than individual files) lets VS Code detect your virtual environment and offer integrated terminal access. All future files you create will live inside this folder.
Your environment is ready. Next up, you will build the resume parser that extracts text from PDF and DOCX files.
Build the Resume Parser
Your Python environment is ready with all the packages installed. The next piece is a module that can actually read resume files and understand what's inside them.
Real ATS systems break resume processing into two stages: text extraction (pulling raw text from PDF or Word files) and structural parsing (figuring out which section is which). You'll build both in this step.
In this step, get ready to:
- Create text extraction functions for PDF and DOCX formats.
- Add section detection logic that identifies resume sections by their headings.
- Add formatting checks and test the parser with a sample file.
Create the text extraction module
Your scanner needs to handle two common resume formats: PDF and DOCX. Each format stores text differently, so you need a dedicated extractor for each one.
- In VS Code, create a new file called resume_parser.py in your ats-scanner folder.
- Add the imports and PDF extraction function by pasting this code:
import pdfplumber
from docx import Document
import re
def extract_text_from_pdf(file) -> str:
"""Extract text from a PDF file using pdfplumber."""
text = ""
with pdfplumber.open(file) as pdf:
for page in pdf.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
return text
What does this code do?
The three imports bring in the libraries you installed earlier: pdfplumber for reading PDFs, Document from python-docx for Word files, and re for regex pattern matching.
The extract_text_from_pdf function opens a PDF, loops through each page, and pulls out the text. The if page_text: check skips empty pages that have no extractable text.
- Save resume_parser.py.
- Confirm the file appears in your VS Code sidebar under the ats-scanner folder.
File not showing in the sidebar?
Make sure you saved the file inside the ats-scanner folder (the same folder open in VS Code). Check that the filename is exactly resume_parser.py with no extra spaces or capitalization.
help me debug why my file isn't appearing in VS Code
- Add the DOCX extraction function below extract_text_from_pdf by pasting this code:
def extract_text_from_docx(file) -> str:
"""Extract text from a DOCX file using python-docx."""
doc = Document(file)
text = ""
for paragraph in doc.paragraphs:
text += paragraph.text + "\n"
return text
What does this code do?
This function uses python-docx to open a Word document and read each paragraph's text content. Word files store text as a list of paragraph objects, so the function loops through them and builds one continuous string.
- Save resume_parser.py.
- Add the dispatcher function below extract_text_from_docx by pasting this code:
def extract_text(file, filename: str) -> str:
"""Extract text from a resume file based on its extension."""
if filename.lower().endswith(".pdf"):
return extract_text_from_pdf(file)
elif filename.lower().endswith(".docx"):
return extract_text_from_docx(file)
else:
raise ValueError(f"Unsupported file format: {filename}")
What does this code do?
The extract_text function is a dispatcher. It checks the file extension and routes to the correct extraction function. If someone uploads a file that isn't PDF or DOCX, it raises a clear error.
- Save resume_parser.py.
- Verify your extraction module works by running this in your terminal:
python -c "from resume_parser import extract_text; print('Text extraction module loaded successfully')"
You should see Text extraction module loaded successfully printed in your terminal.
Seeing an import error?
Make sure your virtual environment is still active (you should see (venv) in your terminal prompt). Also confirm your terminal is in the ats-scanner folder where resume_parser.py lives.
help me fix my import error in resume_parser.py
Add section detection logic
ATS systems don't just read your resume as a wall of text. They look for specific sections like Experience, Education, and Skills, then score each one separately against the job description.
- Add the section patterns dictionary below the extract_text function in resume_parser.py:
SECTION_PATTERNS = {
"experience": r"(?i)\b(experience|work\s*history|employment|professional\s*experience)\b",
"education": r"(?i)\b(education|academic|qualifications|degrees)\b",
"skills": r"(?i)\b(skills|technical\s*skills|competencies|technologies|proficiencies)\b",
"summary": r"(?i)\b(summary|objective|profile|about\s*me|professional\s*summary)\b",
"certifications": r"(?i)\b(certifications|certificates|licenses|credentials)\b",
"projects": r"(?i)\b(projects|portfolio|key\s*projects)\b",
}
What are these patterns?
Each key in SECTION_PATTERNS is a standard resume section name. Each value is a regex pattern that matches common variations of that section heading.
For example, the experience pattern matches "Experience", "Work History", "Employment", and "Professional Experience". The (?i) flag makes each pattern case-insensitive.
- Save resume_parser.py.
- Add the section detection function below SECTION_PATTERNS by pasting this code:
def detect_sections(text: str) -> dict:
"""Detect resume sections and extract their content."""
lines = text.split("\n")
sections = {}
current_section = "header"
current_content = []
for line in lines:
matched_section = None
for section_name, pattern in SECTION_PATTERNS.items():
if re.search(pattern, line) and len(line.split()) <= 5:
matched_section = section_name
break
if matched_section:
if current_content:
sections[current_section] = "\n".join(current_content)
current_section = matched_section
current_content = []
else:
current_content.append(line)
if current_content:
sections[current_section] = "\n".join(current_content)
return sections
How does section detection work?
The function reads the resume line by line. When it finds a line that matches one of the section patterns AND is short enough to be a heading (5 words or fewer), it starts a new section.
Everything before the first detected heading goes into a header section (usually containing the candidate's name and contact info). This line-by-line approach mirrors how real ATS parsers identify section boundaries.
- Save resume_parser.py.
- Verify the section detection loads correctly by running this in your terminal:
python -c "from resume_parser import detect_sections; print('Section detection loaded')"
You should see Section detection loaded printed with no errors.
Seeing a syntax error?
Check that each regex string in SECTION_PATTERNS uses a raw string prefix (r"..."). Also make sure the detect_sections function is not indented inside the dictionary.
help me fix the syntax error in my section detection code
Add format checks and test the parser
ATS systems also penalize resumes with formatting issues. The final function in your parser flags common problems that prevent clean text extraction.
- Add the formatting check function at the bottom of resume_parser.py:
def check_formatting(text: str, sections: dict) -> list:
"""Check for ATS-unfriendly formatting issues."""
issues = []
if len(text.strip()) < 100:
issues.append("Resume appears too short or text extraction failed. Consider using a simpler format.")
standard_sections = ["experience", "education", "skills"]
missing_sections = [s for s in standard_sections if s not in sections]
if missing_sections:
issues.append(f"Missing standard sections: {', '.join(missing_sections)}. ATS systems expect these headings.")
lines = text.split("\n")
short_lines = sum(1 for line in lines if 0 < len(line.strip()) < 3)
if short_lines > 10:
issues.append("Many very short lines detected. This may indicate columns or tables that ATS cannot parse well.")
if re.search(r"[^\x00-\x7F]{5,}", text):
issues.append("Non-ASCII character sequences detected. Some ATS systems struggle with special characters.")
return issues
What does this function check?
- Text length: if fewer than 100 characters were extracted, the PDF might be image-based or corrupted.
- Missing sections: checks for the three most important sections (experience, education, skills).
- Short lines: many very short lines suggest columns or tables that ATS parsers struggle to read.
- Non-ASCII characters: special characters and symbols can confuse some ATS systems.
- Save resume_parser.py.
- Run a full test of your parser with sample text by running this command:
python -c "
from resume_parser import detect_sections, check_formatting
sample = '''John Smith
john@email.com
Experience
Software Engineer at Google
Built microservices with Python
Education
BS Computer Science, MIT 2020
Skills
Python, SQL, Docker, AWS'''
sections = detect_sections(sample)
print('Sections found:', list(sections.keys()))
issues = check_formatting(sample, sections)
print('Format issues:', issues if issues else 'None found')
"
You should see output listing the detected sections (header, experience, education, skills) and either no format issues or a note about missing text length.
Output doesn't match?
If you see a ModuleNotFoundError, confirm your terminal is in the ats-scanner folder and your virtual environment is active. If you see a SyntaxError, double check that your SECTION_PATTERNS dictionary has matching braces and all strings are closed properly.
help me debug my resume parser test output
✔️ Awesome, I've got everything!
Great. Double check you've saved resume_parser.py before moving on.
ⓧ I'd like to double check the full code
Here's the complete resume_parser.py file. Compare yours against it line by line.
import pdfplumber
from docx import Document
import re
def extract_text_from_pdf(file) -> str:
"""Extract text from a PDF file using pdfplumber."""
text = ""
with pdfplumber.open(file) as pdf:
for page in pdf.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
return text
def extract_text_from_docx(file) -> str:
"""Extract text from a DOCX file using python-docx."""
doc = Document(file)
text = ""
for paragraph in doc.paragraphs:
text += paragraph.text + "\n"
return text
def extract_text(file, filename: str) -> str:
"""Extract text from a resume file based on its extension."""
if filename.lower().endswith(".pdf"):
return extract_text_from_pdf(file)
elif filename.lower().endswith(".docx"):
return extract_text_from_docx(file)
else:
raise ValueError(f"Unsupported file format: {filename}")
SECTION_PATTERNS = {
"experience": r"(?i)\b(experience|work\s*history|employment|professional\s*experience)\b",
"education": r"(?i)\b(education|academic|qualifications|degrees)\b",
"skills": r"(?i)\b(skills|technical\s*skills|competencies|technologies|proficiencies)\b",
"summary": r"(?i)\b(summary|objective|profile|about\s*me|professional\s*summary)\b",
"certifications": r"(?i)\b(certifications|certificates|licenses|credentials)\b",
"projects": r"(?i)\b(projects|portfolio|key\s*projects)\b",
}
def detect_sections(text: str) -> dict:
"""Detect resume sections and extract their content."""
lines = text.split("\n")
sections = {}
current_section = "header"
current_content = []
for line in lines:
matched_section = None
for section_name, pattern in SECTION_PATTERNS.items():
if re.search(pattern, line) and len(line.split()) <= 5:
matched_section = section_name
break
if matched_section:
if current_content:
sections[current_section] = "\n".join(current_content)
current_section = matched_section
current_content = []
else:
current_content.append(line)
if current_content:
sections[current_section] = "\n".join(current_content)
return sections
def check_formatting(text: str, sections: dict) -> list:
"""Check for ATS-unfriendly formatting issues."""
issues = []
if len(text.strip()) < 100:
issues.append("Resume appears too short or text extraction failed. Consider using a simpler format.")
standard_sections = ["experience", "education", "skills"]
missing_sections = [s for s in standard_sections if s not in sections]
if missing_sections:
issues.append(f"Missing standard sections: {', '.join(missing_sections)}. ATS systems expect these headings.")
lines = text.split("\n")
short_lines = sum(1 for line in lines if 0 < len(line.strip()) < 3)
if short_lines > 10:
issues.append("Many very short lines detected. This may indicate columns or tables that ATS cannot parse well.")
if re.search(r"[^\x00-\x7F]{5,}", text):
issues.append("Non-ASCII character sequences detected. Some ATS systems struggle with special characters.")
return issues
Your resume parser is complete. It can extract text from both PDF and DOCX files, identify standard resume sections, and flag formatting problems. Next up, you'll build the scoring engine that compares this extracted text against a job description using TF-IDF and cosine similarity.
Build the ATS Scoring Engine
Your resume parser can extract text and detect sections. Now you need the intelligence layer that actually scores how well a resume matches a job description.
Real ATS systems compare resumes to job descriptions using weighted keyword matching. You'll build a scoring engine that uses the same core technique: converting both documents into numerical vectors, then measuring how similar they are.
In this step, get ready to:
- Implement TF-IDF vectorization to convert text into comparable numerical vectors.
- Calculate cosine similarity to produce an overall match score from 0 to 100.
- Build keyword extraction that identifies matched and missing keywords from the job description.
Implement TF-IDF vectorization with cosine similarity scoring
TF-IDF (Term Frequency-Inverse Document Frequency) converts raw text into a vector of numbers. Each number represents how important a word is to a document relative to a collection. Cosine similarity then measures the angle between two vectors to determine how similar the documents are.
- Create a new file called scoring_engine.py in your ats-scanner folder.
- Add the imports and text preprocessing function by pasting this code:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import re
def preprocess_text(text: str) -> str:
"""Clean and normalize text for comparison."""
# Convert to lowercase for consistent matching
text = text.lower()
# Remove everything except letters, numbers, spaces, and common tech symbols
text = re.sub(r"[^a-z0-9\s\+\#\.]", " ", text)
# Collapse multiple spaces into one
text = re.sub(r"\s+", " ", text)
return text.strip()
What does this code do?
The imports bring in TfidfVectorizer (converts text to vectors) and cosine_similarity (measures vector similarity) from scikit-learn.
The preprocess_text function normalizes text before comparison. It lowercases everything, keeps only alphanumeric characters plus +, #, and . (common in tech terms like C++ or C#), and collapses whitespace.
- Below preprocess_text, add the main scoring function that performs TF-IDF vectorization and cosine similarity:
def calculate_match_score(resume_text: str, job_description: str) -> float:
"""Calculate TF-IDF cosine similarity between resume and job description."""
processed_resume = preprocess_text(resume_text)
processed_jd = preprocess_text(job_description)
# Configure the vectorizer with bigrams and English stop word removal
vectorizer = TfidfVectorizer(
stop_words="english",
ngram_range=(1, 2),
max_features=5000,
sublinear_tf=True,
)
# Convert both texts into TF-IDF vectors
tfidf_matrix = vectorizer.fit_transform([processed_resume, processed_jd])
# Measure how similar the two vectors are
similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])
# Scale from 0-1 to 0-100 and round
score = round(float(similarity[0][0]) * 100, 1)
return score
What does this code do?
- stop_words="english" removes common words like "the" and "is" that add noise.
- ngram_range=(1, 2) captures both single words and two-word phrases (like "machine learning").
- max_features=5000 limits the vocabulary to the 5000 most important terms for performance.
- sublinear_tf=True applies logarithmic scaling so a word appearing 10 times isn't rated 10x more important than one appearing once.
- cosine_similarity returns a value between 0 (no match) and 1 (identical). Multiplying by 100 gives a human-readable percentage score.
- Save scoring_engine.py.
- Test the scoring function by running this command in your terminal:
python -c "from scoring_engine import calculate_match_score; print(calculate_match_score('experienced python developer with machine learning skills', 'looking for a python developer with machine learning experience'))"
- You should see a number between 40 and 80 printed to the terminal. The exact value depends on how TF-IDF weights these short sample texts.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
- Confirm you installed scikit-learn in Step 1. If not, run pip install scikit-learn now.
- Make sure you're running the command from inside the ats-scanner folder where scoring_engine.py lives.
Help me debug a ModuleNotFoundError when importing scoring_engine
Build keyword extraction and matching
A match score tells you how similar two documents are overall. But job seekers also need to know which specific keywords they're missing so they can improve their resume.
- In scoring_engine.py, add the keyword extraction function below calculate_match_score:
def extract_keywords(text: str, top_n: int = 30) -> set:
"""Extract important keywords from text using TF-IDF scores."""
processed = preprocess_text(text)
# Use a smaller feature set for keyword extraction
vectorizer = TfidfVectorizer(
stop_words="english",
ngram_range=(1, 2),
max_features=500,
sublinear_tf=True,
)
tfidf_matrix = vectorizer.fit_transform([processed])
feature_names = vectorizer.get_feature_names_out()
scores = tfidf_matrix.toarray()[0]
# Pair each keyword with its TF-IDF score and sort by importance
keyword_scores = list(zip(feature_names, scores))
keyword_scores.sort(key=lambda x: x[1], reverse=True)
# Keep only meaningful keywords (length > 2, non-zero score)
keywords = set()
for keyword, score in keyword_scores[:top_n]:
if len(keyword) > 2 and score > 0:
keywords.add(keyword)
return keywords
What does this code do?
This function runs TF-IDF on a single document to find its most important terms. It ranks all terms by their TF-IDF score (how distinctive they are) and returns the top N as a set. The len(keyword) > 2 filter removes trivial two-character tokens that slip through stop-word removal.
- Below extract_keywords, add the function that compares job description keywords against the resume:
def analyze_keywords(resume_text: str, job_description: str) -> dict:
"""Compare keywords between resume and job description."""
jd_keywords = extract_keywords(job_description, top_n=30)
resume_keywords = extract_keywords(resume_text, top_n=50)
processed_resume = preprocess_text(resume_text)
matched = set()
missing = set()
# Check which job description keywords appear in the resume text
for keyword in jd_keywords:
if keyword in processed_resume:
matched.add(keyword)
else:
missing.add(keyword)
return {
"matched": sorted(matched),
"missing": sorted(missing),
"jd_keywords": sorted(jd_keywords),
"match_percentage": round(len(matched) / max(len(jd_keywords), 1) * 100, 1),
}
What does this code do?
This function extracts the top 30 keywords from the job description and checks which ones appear in the resume's preprocessed text. It returns a dictionary with matched keywords, missing keywords, and a match percentage. The max(len(jd_keywords), 1) prevents a division-by-zero error if no keywords are extracted.
- Save scoring_engine.py.
Seeing a syntax error?
- Check that you have a blank line between calculate_match_score and extract_keywords. Python expects spacing between top-level function definitions.
- Make sure the return statement in analyze_keywords uses curly braces {} for the dictionary, not parentheses.
Help me fix a syntax error in my scoring_engine.py keyword functions
Add section scoring and suggestion generation
A single overall score is useful, but it doesn't tell you which parts of your resume are strong or weak. Section-level scoring lets you pinpoint exactly where to focus your improvements.
- Below analyze_keywords, add the section scoring function:
def score_sections(sections: dict, job_description: str) -> dict:
"""Score individual resume sections against the job description."""
section_scores = {}
for section_name, section_content in sections.items():
# Skip the header section (name, contact info)
if section_name == "header":
continue
# Skip sections that are too short to score meaningfully
if len(section_content.strip()) < 20:
section_scores[section_name] = 0.0
continue
# Reuse calculate_match_score for each section
score = calculate_match_score(section_content, job_description)
section_scores[section_name] = score
return section_scores
What does this code do?
This function takes the sections dictionary from detect_sections (built in Step 2) and scores each one individually against the job description. Sections shorter than 20 characters get a zero score since there isn't enough text for meaningful comparison. The header is skipped because it contains contact info, not job-relevant content.
- Add the final function that generates actionable suggestions based on all the analysis:
def generate_suggestions(score: float, keyword_analysis: dict, format_issues: list) -> list:
"""Generate actionable suggestions based on the analysis."""
suggestions = []
# Suggest based on overall score thresholds
if score < 50:
suggestions.append("Your overall match score is low. Consider tailoring your resume more closely to this specific job description.")
elif score < 75:
suggestions.append("Your match score is moderate. Adding more relevant keywords could push you above the 75% threshold that many recruiters use as a cutoff.")
# Suggest adding missing keywords
missing = keyword_analysis.get("missing", [])
if missing:
top_missing = missing[:5]
suggestions.append(f"Consider adding these missing keywords (if they reflect your actual skills): {', '.join(top_missing)}")
# Flag formatting problems
if format_issues:
suggestions.append("Fix formatting issues: " + format_issues[0])
# Warn if keyword match is below threshold
if keyword_analysis.get("match_percentage", 0) < 60:
suggestions.append("Your keyword match percentage is below 60%. Mirror the exact language from the job description where it honestly reflects your experience.")
return suggestions
What does this code do?
This function checks score thresholds and keyword analysis results to build a list of concrete actions the user can take. The 75% threshold reflects the cutoff many recruiters use when filtering ATS results. Each suggestion is conditional, so the learner only sees advice that's relevant to their specific resume.
- Save scoring_engine.py.
- Verify the complete module loads correctly by running:
python -c "from scoring_engine import calculate_match_score, analyze_keywords, score_sections, generate_suggestions; print('All functions imported successfully')"
- You should see All functions imported successfully printed in your terminal.
Import failing?
- Check that all six functions are in scoring_engine.py and that each starts with def at the leftmost indentation level (no extra spaces before def).
- Make sure there is exactly one blank line between each function definition.
- Confirm the file is saved in the same folder as resume_parser.py.
Help me fix an import error in scoring_engine.py
✔️ Awesome, I've got everything!
Great. Double check you've saved scoring_engine.py and the import test prints the success message.
ⓧ I'd like to double check the full code
Here's the complete scoring_engine.py file:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import re
def preprocess_text(text: str) -> str:
"""Clean and normalize text for comparison."""
text = text.lower()
text = re.sub(r"[^a-z0-9\s\+\#\.]", " ", text)
text = re.sub(r"\s+", " ", text)
return text.strip()
def calculate_match_score(resume_text: str, job_description: str) -> float:
"""Calculate TF-IDF cosine similarity between resume and job description."""
processed_resume = preprocess_text(resume_text)
processed_jd = preprocess_text(job_description)
vectorizer = TfidfVectorizer(
stop_words="english",
ngram_range=(1, 2),
max_features=5000,
sublinear_tf=True,
)
tfidf_matrix = vectorizer.fit_transform([processed_resume, processed_jd])
similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])
score = round(float(similarity[0][0]) * 100, 1)
return score
def extract_keywords(text: str, top_n: int = 30) -> set:
"""Extract important keywords from text using TF-IDF scores."""
processed = preprocess_text(text)
vectorizer = TfidfVectorizer(
stop_words="english",
ngram_range=(1, 2),
max_features=500,
sublinear_tf=True,
)
tfidf_matrix = vectorizer.fit_transform([processed])
feature_names = vectorizer.get_feature_names_out()
scores = tfidf_matrix.toarray()[0]
keyword_scores = list(zip(feature_names, scores))
keyword_scores.sort(key=lambda x: x[1], reverse=True)
keywords = set()
for keyword, score in keyword_scores[:top_n]:
if len(keyword) > 2 and score > 0:
keywords.add(keyword)
return keywords
def analyze_keywords(resume_text: str, job_description: str) -> dict:
"""Compare keywords between resume and job description."""
jd_keywords = extract_keywords(job_description, top_n=30)
resume_keywords = extract_keywords(resume_text, top_n=50)
processed_resume = preprocess_text(resume_text)
matched = set()
missing = set()
for keyword in jd_keywords:
if keyword in processed_resume:
matched.add(keyword)
else:
missing.add(keyword)
return {
"matched": sorted(matched),
"missing": sorted(missing),
"jd_keywords": sorted(jd_keywords),
"match_percentage": round(len(matched) / max(len(jd_keywords), 1) * 100, 1),
}
def score_sections(sections: dict, job_description: str) -> dict:
"""Score individual resume sections against the job description."""
section_scores = {}
for section_name, section_content in sections.items():
if section_name == "header":
continue
if len(section_content.strip()) < 20:
section_scores[section_name] = 0.0
continue
score = calculate_match_score(section_content, job_description)
section_scores[section_name] = score
return section_scores
def generate_suggestions(score: float, keyword_analysis: dict, format_issues: list) -> list:
"""Generate actionable suggestions based on the analysis."""
suggestions = []
if score < 50:
suggestions.append("Your overall match score is low. Consider tailoring your resume more closely to this specific job description.")
elif score < 75:
suggestions.append("Your match score is moderate. Adding more relevant keywords could push you above the 75% threshold that many recruiters use as a cutoff.")
missing = keyword_analysis.get("missing", [])
if missing:
top_missing = missing[:5]
suggestions.append(f"Consider adding these missing keywords (if they reflect your actual skills): {', '.join(top_missing)}")
if format_issues:
suggestions.append("Fix formatting issues: " + format_issues[0])
if keyword_analysis.get("match_percentage", 0) < 60:
suggestions.append("Your keyword match percentage is below 60%. Mirror the exact language from the job description where it honestly reflects your experience.")
return suggestions
Why TF-IDF instead of deep learning?
Classical NLP methods like TF-IDF are faster, interpretable, and require no GPU. They closely mirror what many real ATS systems still use under the hood.
TF-IDF is lexical, not semantic. "Machine Learning" and "ML" score as separate terms. A production system would add synonym expansion or embeddings on top. This is a known limitation you could extend in a future project.
Your scoring engine is complete. Next up, you'll wire these functions into a Streamlit web interface so you can upload real resumes and see results visually.
Add Section Analysis and Format Checks
In the previous step, you built the core scoring engine with TF-IDF and keyword analysis. You also wrote functions that score individual resume sections and generate improvement suggestions.
Now it is time to verify that the full analysis pipeline works end-to-end. You will test section-level scoring, formatting checks, and suggestion generation with sample data to confirm everything connects correctly before building the web interface.
In this step, get ready to:
- Test section-level scoring against a sample job description.
- Verify formatting checks flag ATS-unfriendly patterns.
- Run the suggestion generator end-to-end with combined inputs.
Test section-level scoring with sample data
The score_sections function takes the dictionary of detected resume sections and scores each one individually against the job description. This tells the user which parts of their resume are strong and which need work.
- Open your terminal in VS Code (the one with your virtual environment active).
- Start the Python interactive interpreter by running:
python3
- Import your modules and set up sample data by entering this code in the interpreter:
from resume_parser import detect_sections, check_formatting
from scoring_engine import score_sections, generate_suggestions, analyze_keywords, calculate_match_score
# Sample resume text with clear sections
resume_text = """Summary
Experienced software engineer with 5 years in Python and cloud computing.
Experience
Senior Developer at TechCorp. Built REST APIs using Python Flask.
Deployed microservices on AWS using Docker and Kubernetes.
Skills
Python, Flask, Django, AWS, Docker, Kubernetes, PostgreSQL, Git
Education
BS Computer Science, State University, 2018"""
# Sample job description
job_description = """We are looking for a Senior Python Developer with experience in
REST APIs, cloud computing, AWS, Docker, and Kubernetes.
Must have strong Python skills and experience with microservices.
Django or Flask experience preferred. PostgreSQL knowledge required."""
# Detect sections from the resume
sections = detect_sections(resume_text)
print("Detected sections:", list(sections.keys()))
What does this code do?
You import both modules you have built so far. The sample resume text uses standard section headers that detect_sections recognizes.
The detect_sections function splits the resume into named chunks (summary, experience, skills, education) by matching regex patterns against the header lines.
- You should see output similar to:
Detected sections: ['header', 'summary', 'experience', 'skills', 'education']
- Now score each section against the job description by entering:
# Score each resume section individually
section_scores = score_sections(sections, job_description)
for name, score in section_scores.items():
print(f" {name}: {score}/100")
What should I see?
Each section gets its own match score from 0 to 100. The skills section will likely score highest because it contains the exact keywords from the job description.
Sections with fewer than 20 characters of content are automatically scored at 0.0 because there is not enough text for meaningful comparison.
Seeing an ImportError?
Make sure your virtual environment is active (you should see (venv) in your terminal prompt) and that you started the interpreter from inside the ats-scanner folder.
If you see a ModuleNotFoundError for scikit-learn, run pip install scikit-learn in your virtual environment first.
help me debug an ImportError in my Python REPL
Verify formatting checks flag ATS-unfriendly patterns
Real ATS systems reject resumes with layout problems they cannot parse. The check_formatting function inspects the raw text for common red flags like missing standard sections, very short lines (indicating columns or tables), and non-ASCII characters.
- Test the formatting check with your well-structured sample resume by entering:
# Check formatting on the well-structured resume
format_issues = check_formatting(resume_text, sections)
print("Format issues found:", len(format_issues))
for issue in format_issues:
print(f" - {issue}")
What should I see?
Your well-structured sample resume should have zero or very few issues. The sample has all three standard sections (experience, education, skills) and uses plain text formatting.
- Now test with a problematic resume to see the checks catch real issues:
# Test with a poorly formatted resume (missing sections, too short)
bad_resume = "Hi I am John"
bad_sections = detect_sections(bad_resume)
bad_issues = check_formatting(bad_resume, bad_sections)
print("Issues with bad resume:", len(bad_issues))
for issue in bad_issues:
print(f" - {issue}")
- You should see multiple issues flagged, including that the resume is too short and missing standard sections.
Why does this matter for ATS?
Real ATS systems check for parseability before scoring. A resume that uses columns, tables, or images might look great to a human reader but produces garbled text when an ATS extracts it.
By checking for these patterns upfront, your scanner gives users the same early warning a recruiter's tool would.
Run the full suggestion pipeline end-to-end
The generate_suggestions function ties everything together. It takes the overall score, keyword analysis, and format issues, then produces a list of specific actions the user can take to improve their resume.
- Run the complete analysis pipeline by entering:
# Run the full pipeline
overall_score = calculate_match_score(resume_text, job_description)
keyword_analysis = analyze_keywords(resume_text, job_description)
format_issues = check_formatting(resume_text, sections)
print(f"Overall match score: {overall_score}/100")
print(f"Keywords matched: {keyword_analysis['match_percentage']}%")
print(f"Matched: {keyword_analysis['matched'][:5]}")
print(f"Missing: {keyword_analysis['missing'][:5]}")
print(f"Format issues: {len(format_issues)}")
What does this output tell me?
The overall score uses cosine similarity between the full resume and job description. Typical TF-IDF scores for normal resumes fall in the 20-60 range, so do not expect 90+ here.
The keyword analysis shows you exactly which terms from the job description appear in the resume and which are missing. This is the most actionable feedback for a job seeker.
- Finally, generate the improvement suggestions by entering:
# Generate actionable suggestions
suggestions = generate_suggestions(overall_score, keyword_analysis, format_issues)
print(f"\nSuggestions ({len(suggestions)}):")
for i, suggestion in enumerate(suggestions, 1):
print(f" {i}. {suggestion}")
- You should see 1-4 suggestions depending on your score and keyword match percentage.
- Exit the Python interpreter by pressing Ctrl+D (macOS/Linux) or typing exit() (Windows).
How does suggestion generation work?
The function uses threshold-based logic. If your overall score is below 50, it suggests tailoring the resume more closely. If it is between 50-75, it notes you are close to the 75% threshold many recruiters use as a cutoff.
It also surfaces the top 5 missing keywords and flags the first formatting issue, giving users concrete next steps rather than vague advice.
Not seeing any suggestions?
If your sample resume closely matches the job description, the score might be above 75 and keyword match above 60%. In that case, the function correctly returns fewer suggestions because the resume is already well-matched.
Try testing with a resume that has no overlapping keywords to see the full range of suggestions fire.
help me understand why generate_suggestions returns an empty list
Your analysis pipeline is verified and working. Next up, you will build the Streamlit web interface that brings all of these functions together into an interactive dashboard.
Build the Streamlit Web Interface
Your resume parser extracts text and your scoring engine crunches the numbers. Now you need an interface that ties them together so you can actually use your scanner.
Streamlit is a Python framework that turns scripts into interactive web apps with zero front-end code. You'll build a dashboard where you upload a resume, paste a job description, and instantly see your match score, keyword breakdown, and improvement suggestions.
In this step, get ready to:
- Create the app layout with file upload and job description inputs.
- Wire up the scoring engine to display results with progress bars and color-coded scores.
- Add the keyword analysis view showing matched and missing terms.
Create the app layout with file upload and text input
Streamlit apps are just Python scripts that run top to bottom. Each st. call adds a widget to the page in the order you write it.
- In VS Code, create a new file called app.py in your ats-scanner folder.
- Add the imports and page configuration by pasting this code:
import streamlit as st
from resume_parser import extract_text, detect_sections, check_formatting
from scoring_engine import (
calculate_match_score,
analyze_keywords,
score_sections,
generate_suggestions,
)
# Configure the page layout and title
st.set_page_config(page_title="ATS Resume Scanner", page_icon="\ud83d\udcc4", layout="wide")
st.title("\ud83d\udcc4 ATS Resume Scanner")
st.markdown("Upload your resume and paste a job description to see how well they match.")
What does this code do?
The imports pull in every function you built in the previous steps. st.set_page_config sets the browser tab title, icon, and uses a wide layout so the dashboard has room to breathe.
st.title and st.markdown add the heading and subtitle that greet users when they open the app.
Now you need two side-by-side inputs: one for the resume file and one for the job description text.
- Add the two-column input layout below your existing code:
# Create two columns for resume upload and job description input
col1, col2 = st.columns(2)
with col1:
st.subheader("Upload Resume")
uploaded_file = st.file_uploader(
"Choose a PDF or DOCX file",
type=["pdf", "docx"],
help="Upload your resume in PDF or DOCX format",
)
with col2:
st.subheader("Job Description")
job_description = st.text_area(
"Paste the job description here",
height=300,
placeholder="Paste the full job description including requirements, qualifications, and responsibilities...",
)
What does this code do?
st.columns(2) splits the page into two equal columns. The left column gets a file uploader restricted to PDF and DOCX formats. The right column gets a tall text area for pasting job descriptions.
The type=["pdf", "docx"] parameter means Streamlit will reject any other file types automatically.
- Save app.py.
Seeing an import error?
Make sure your virtual environment is still active. You should see (venv) in your terminal prompt. If not, reactivate it before running the app.
Help me fix my import errors in app.py
Wire up the scoring engine with visual indicators
The input layout is ready. Now you need to connect the Analyze button to your scoring engine and display the results with color-coded feedback.
- Add the analyze button and scoring logic below your column layout in app.py:
# Only run analysis when both inputs are provided
if uploaded_file and job_description:
if st.button("\ud83d\udd0d Analyze Resume", type="primary"):
with st.spinner("Analyzing your resume..."):
# Extract text and detect sections
resume_text = extract_text(uploaded_file, uploaded_file.name)
sections = detect_sections(resume_text)
format_issues = check_formatting(resume_text, sections)
# Run the scoring engine
overall_score = calculate_match_score(resume_text, job_description)
keyword_analysis = analyze_keywords(resume_text, job_description)
section_scores = score_sections(sections, job_description)
suggestions = generate_suggestions(overall_score, keyword_analysis, format_issues)
What does this code do?
The outer if check ensures the button only appears when both a file and job description are present. st.spinner shows a loading animation while the analysis runs.
Inside the spinner, the code calls every function you built: text extraction, section detection, formatting checks, match scoring, keyword analysis, section scoring, and suggestion generation.
- Add the score display section directly below (still inside the button's if block):
st.divider()
# Display overall score and keyword match side by side
score_col, keyword_col = st.columns(2)
with score_col:
st.subheader("Overall Match Score")
if overall_score >= 75:
st.success(f"\ud83c\udfaf {overall_score}/100")
elif overall_score >= 50:
st.warning(f"\u26a0\ufe0f {overall_score}/100")
else:
st.error(f"\u274c {overall_score}/100")
st.progress(overall_score / 100)
st.caption("Aim for 75+ to pass most ATS filters.")
with keyword_col:
st.subheader("Keyword Match")
match_pct = keyword_analysis["match_percentage"]
st.metric("Keywords Matched", f"{match_pct}%")
st.progress(match_pct / 100)
What does this code do?
The score display uses conditional formatting. Scores at or above 75 get a green success box. Scores between 50 and 75 get an amber warning. Anything below 50 gets a red error indicator.
st.progress renders a visual progress bar, and st.metric displays the keyword match percentage as a large highlighted number.
- Save app.py.
Seeing indentation errors?
The score display code is indented 8 spaces (two levels) because it sits inside both the if st.button block and the outer if uploaded_file and job_description block. Make sure your indentation matches.
Help me fix indentation in my Streamlit app
Add the keyword analysis view and status messages
The score dashboard gives a high-level number. Now you need the detail view that shows exactly which keywords matched and which are missing.
- Add the keyword columns below the score display (still inside the button's if block, at the same indentation level):
st.divider()
# Show matched and missing keywords in two columns
kw_col1, kw_col2 = st.columns(2)
with kw_col1:
st.subheader("\u2705 Matched Keywords")
if keyword_analysis["matched"]:
st.write(", ".join(keyword_analysis["matched"]))
else:
st.write("No keyword matches found.")
with kw_col2:
st.subheader("\u274c Missing Keywords")
if keyword_analysis["missing"]:
st.write(", ".join(keyword_analysis["missing"]))
else:
st.write("No missing keywords. Great job!")
What does this code do?
This section splits keywords into two columns. The left shows keywords from the job description that appear in your resume. The right shows keywords you're missing. These lists give you concrete actions to improve your score.
- Add the section scores, formatting issues, suggestions, and status messages to complete the app:
st.divider()
# Display per-section scores with progress bars
st.subheader("\ud83d\udcca Section Scores")
if section_scores:
for section_name, score in section_scores.items():
col_a, col_b = st.columns([1, 3])
with col_a:
st.write(f"**{section_name.title()}**")
with col_b:
st.progress(score / 100)
st.caption(f"{score}/100")
else:
st.info("No standard sections detected. Consider using clear section headings like 'Experience', 'Education', 'Skills'.")
# Show formatting issues as warnings
if format_issues:
st.divider()
st.subheader("\u26a0\ufe0f Formatting Issues")
for issue in format_issues:
st.warning(issue)
# Show improvement suggestions
if suggestions:
st.divider()
st.subheader("\ud83d\udca1 Suggestions")
for suggestion in suggestions:
st.info(suggestion)
# Status messages when inputs are incomplete
elif uploaded_file and not job_description:
st.info("\ud83d\udc46 Paste a job description to start the analysis.")
elif job_description and not uploaded_file:
st.info("\ud83d\udc46 Upload your resume to start the analysis.")
else:
st.info("\ud83d\udc46 Upload your resume and paste a job description to get started.")
What does this code do?
The section scores loop creates a progress bar for each resume section (Experience, Skills, Education, etc.) so you can see which areas are strong and which need work.
The elif/else block at the bottom handles incomplete inputs. It shows helpful prompts depending on what's missing, guiding the user to provide both a resume and a job description.
- Save app.py.
- Launch your app by running this command in your terminal:
streamlit run app.py
Your browser should open automatically to http://localhost:8501. You should see the ATS Resume Scanner title with two input columns.
- Upload a resume (PDF or DOCX) in the left column.
- Paste a job description in the right column.
- Click the Analyze Resume button.
You should see your overall match score (color-coded), keyword match percentage, matched and missing keyword lists, section scores with progress bars, and any suggestions for improvement.
App not loading in your browser?
If the browser didn't open automatically, go to http://localhost:8501 manually.
If you see a ModuleNotFoundError, make sure your virtual environment is active and that you installed all packages in Step 1.
Help me get my Streamlit app running
Test with a real resume
For the best test, upload your actual resume and paste a job description you're interested in. The scanner works best with real content because TF-IDF scoring relies on meaningful word frequency patterns.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import streamlit as st
from resume_parser import extract_text, detect_sections, check_formatting
from scoring_engine import (
calculate_match_score,
analyze_keywords,
score_sections,
generate_suggestions,
)
st.set_page_config(page_title="ATS Resume Scanner", page_icon="\ud83d\udcc4", layout="wide")
st.title("\ud83d\udcc4 ATS Resume Scanner")
st.markdown("Upload your resume and paste a job description to see how well they match.")
col1, col2 = st.columns(2)
with col1:
st.subheader("Upload Resume")
uploaded_file = st.file_uploader(
"Choose a PDF or DOCX file",
type=["pdf", "docx"],
help="Upload your resume in PDF or DOCX format",
)
with col2:
st.subheader("Job Description")
job_description = st.text_area(
"Paste the job description here",
height=300,
placeholder="Paste the full job description including requirements, qualifications, and responsibilities...",
)
if uploaded_file and job_description:
if st.button("\ud83d\udd0d Analyze Resume", type="primary"):
with st.spinner("Analyzing your resume..."):
resume_text = extract_text(uploaded_file, uploaded_file.name)
sections = detect_sections(resume_text)
format_issues = check_formatting(resume_text, sections)
overall_score = calculate_match_score(resume_text, job_description)
keyword_analysis = analyze_keywords(resume_text, job_description)
section_scores = score_sections(sections, job_description)
suggestions = generate_suggestions(overall_score, keyword_analysis, format_issues)
st.divider()
score_col, keyword_col = st.columns(2)
with score_col:
st.subheader("Overall Match Score")
if overall_score >= 75:
st.success(f"\ud83c\udfaf {overall_score}/100")
elif overall_score >= 50:
st.warning(f"\u26a0\ufe0f {overall_score}/100")
else:
st.error(f"\u274c {overall_score}/100")
st.progress(overall_score / 100)
st.caption("Aim for 75+ to pass most ATS filters.")
with keyword_col:
st.subheader("Keyword Match")
match_pct = keyword_analysis["match_percentage"]
st.metric("Keywords Matched", f"{match_pct}%")
st.progress(match_pct / 100)
st.divider()
kw_col1, kw_col2 = st.columns(2)
with kw_col1:
st.subheader("\u2705 Matched Keywords")
if keyword_analysis["matched"]:
st.write(", ".join(keyword_analysis["matched"]))
else:
st.write("No keyword matches found.")
with kw_col2:
st.subheader("\u274c Missing Keywords")
if keyword_analysis["missing"]:
st.write(", ".join(keyword_analysis["missing"]))
else:
st.write("No missing keywords. Great job!")
st.divider()
st.subheader("\ud83d\udcca Section Scores")
if section_scores:
for section_name, score in section_scores.items():
col_a, col_b = st.columns([1, 3])
with col_a:
st.write(f"**{section_name.title()}**")
with col_b:
st.progress(score / 100)
st.caption(f"{score}/100")
else:
st.info("No standard sections detected. Consider using clear section headings like 'Experience', 'Education', 'Skills'.")
if format_issues:
st.divider()
st.subheader("\u26a0\ufe0f Formatting Issues")
for issue in format_issues:
st.warning(issue)
if suggestions:
st.divider()
st.subheader("\ud83d\udca1 Suggestions")
for suggestion in suggestions:
st.info(suggestion)
elif uploaded_file and not job_description:
st.info("\ud83d\udc46 Paste a job description to start the analysis.")
elif job_description and not uploaded_file:
st.info("\ud83d\udc46 Upload your resume to start the analysis.")
else:
st.info("\ud83d\udc46 Upload your resume and paste a job description to get started.")
Your ATS scanner is fully functional. You can now analyze any resume against any job description and get instant feedback on how to improve your match score. Next up, you'll extend this into a multi-resume comparison tool in the Secret Mission.
Secret mission
Multi-Resume Comparison Mode
You've built a scanner that evaluates one resume at a time. But what if you have multiple versions of your resume tailored to different angles? In this secret mission, you'll add a comparison mode that scores all your resume versions against the same job description and ranks them side by side.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The ats-scanner project folder on your Desktop (contains app.py, resume_parser.py, scoring_engine.py).
- The Python virtual environment inside ats-scanner.
- Installed packages (streamlit, scikit-learn, pdfplumber, python-docx) inside the virtual environment.
Keep everything running
No action needed. Choose this if you're still actively building or plan to use your ATS scanner before your next job application.
- Your project files stay on your Desktop.
- Your virtual environment stays ready. Reactivate it anytime and run streamlit run app.py to launch the scanner.
Pause - I'll come back to this later
Shut down the Streamlit server and deactivate your virtual environment to free up memory. Your files stay intact for next time.
- If the Streamlit server is still running in your terminal, press Ctrl+C to stop it.
- Deactivate your virtual environment by running:
deactivate
Your project folder and all files remain on your Desktop. When you come back, reactivate the environment and relaunch the app.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes everything including your virtual environment and installed packages.
- If the Streamlit server is still running, press Ctrl+C to stop it.
- Deactivate your virtual environment by running:
deactivate
- Delete the entire project folder by running:
macOS
cd ~/Desktop
rm -rf ats-scanner
Windows
cd ~\Desktop
rmdir /s /q ats-scanner
- Confirm the folder is gone by running:
ls ~/Desktop
You should no longer see ats-scanner in the output.
Nice Work!
Nice Work!
You just built your own ATS resume scanner that uses real NLP techniques to score resumes against job descriptions. This is the same core logic that enterprise hiring tools use to filter candidates.
Here's what you learned:
- Built a resume parser using pdfplumber and python-docx that extracts structured text from PDF and DOCX files.
- Implemented TF-IDF vectorization and cosine similarity scoring to match resumes against any job description.
- Deployed an interactive Streamlit web app with keyword analysis, section-level scoring, and actionable improvement suggestions.
- Added a multi-resume comparison mode to upload multiple resume versions and see which one scores highest against the same job description.
Ready to quiz yourself?