Unit Testing in Python with pytest

Write a pytest test suite that validates healthcare NLP functions.

Introduction

30 Second Summary

When an AI system processes a doctor's clinical notes, a single missed medication or misidentified section could mean a dangerous drug interaction goes unnoticed. The code behind healthcare AI needs a safety net that catches these failures before they reach patients.

In this project, you will build a unit test suite using pytest to validate healthcare NLP functions that process clinical notes.

What You'll Build

A test suite for a mini healthcare NLP module that detects section headers in clinical notes and extracts medication information using regex.

By the end of this project, you'll have:

  • A clinical_nlp.py module with functions for section detection and medication extraction from clinical text.
  • A test_clinical_nlp.py file with 10+ unit tests covering normal cases, edge cases, and error handling.
  • Experience reading pytest failure output and using it to locate and fix bugs in your code.
  • Secret Mission: Add parametrized tests that validate your extractor against a batch of real-world-style clinical note snippets.

Are there any prerequisites?

You need basic Python knowledge (functions, strings, and lists). No prior testing experience is needed.

Before We Start

Before We Start...

Set Up Python and pytest

Before you can write tests, you need a working Python environment and the pytest framework installed.

In this step, you'll verify your Python version, create a dedicated project folder, install pytest, and set up the two files you'll work with throughout this project.

In this step, get ready to:
  • Verify Python 3.10 or higher is installed.
  • Create a project folder and install pytest.
  • Create the two project files.
Verify Python is installed

pytest 9.x requires Python 3.10 or higher. Let's confirm you have a compatible version before going further.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check your Python version by running the command for your operating system:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.10 or higher

Your Python version is compatible with pytest 9.x. You're good to move on.

ⓧ I see an older version

You need Python 3.10 or higher. Your current version won't work with pytest 9.x.

  • Download the latest Python installer from python.org/downloads.
  • Run the installer. On Windows, check Add Python to PATH before clicking Install Now.
  • Close and reopen your terminal, then run the version check again to confirm the new version appears.

macOS default Python

macOS may still resolve python3 to the older system version after installing a new one. If that happens, use the full path to the new installation (e.g. /usr/local/bin/python3) or install via a version manager like pyenv.

ⓧ Command not found

Python isn't installed on your machine yet. Let's fix that.

  • Go to python.org/downloads and download the latest Python 3 installer.
  • Run the installer. On Windows, check Add Python to PATH before clicking Install Now.
  • Close and reopen your terminal after installation completes.
  • Run the version check command again. You should now see a version number starting with 3.10 or higher.
Create a project folder and install pytest
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir healthcare-nlp-tests
cd healthcare-nlp-tests
  • 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 healthcare-nlp-tests folder, and click Open.
  • Open the integrated terminal by pressing Ctrl+` (backtick key, located above Tab on most keyboards).

What is the integrated terminal?

The integrated terminal is a command line panel built into VS Code. It opens directly inside your project folder, so you don't need to cd anywhere. You'll use it for running pip installs, pytest commands, and Python scripts throughout this project.

  • Install pytest by running this command in the integrated terminal:
pip install pytest==9.0.3

What does this command do?

pip install pytest==9.0.3 downloads and installs version 9.0.3 of the pytest framework. Pinning to a specific version ensures your setup matches this project exactly.

  • Verify pytest installed correctly by running:
pytest --version

You should see a version number that includes 9.0.3 in the output.

Seeing 'command not found' for pip or pytest?

On macOS, try pip3 instead of pip. Some systems install the Python 3 package manager under that name.

On Windows, make sure you checked Add Python to PATH during installation. If you missed it, rerun the Python installer and select Modify to add it.

help me fix pip or pytest not being recognized in my terminal

Create the project files

pytest automatically discovers test files by looking for filenames that start with test_. Following this naming convention means you never need to tell pytest where your tests live.

  • Create the two project files by running the command for your operating system:

macOS

touch clinical_nlp.py test_clinical_nlp.py

Windows

New-Item -Path clinical_nlp.py -ItemType File
New-Item -Path test_clinical_nlp.py -ItemType File

What are these two files for?

clinical_nlp.py is where you'll write the NLP functions that process clinical text. test_clinical_nlp.py is where you'll write tests that verify those functions work correctly.

Keeping the code and tests in separate files is standard practice. It lets you run your test suite independently without executing your main application.

  • Confirm the files exist by running:
ls

You should see clinical_nlp.py and test_clinical_nlp.py in the output.

Don't see both files?

Make sure your terminal is inside the healthcare-nlp-tests folder. Run pwd (macOS) or cd (Windows) to check your current directory. The path should end with healthcare-nlp-tests.

help me figure out why my project files weren't created

Your environment is ready. Next up, you'll write your first NLP function and see what a passing test looks like.

Write Your First NLP Function and Test

Your project folder is ready and pytest is installed. Now it's time to write code that actually does something testable.

Clinical notes contain section headers like "MEDICATIONS:" and "ASSESSMENT:" that organize patient information. You'll write a function that detects these headers, then write a test that proves it works.

In this step, get ready to:
  • Write a detect_section function that identifies clinical note headers.
  • Write your first pytest test to verify the function works.
  • Intentionally break the function to explore pytest's failure output.
Write a section detection function

Clinical notes are organized into sections. Each section starts with a header like "MEDICATIONS:" or "ASSESSMENT:" followed by a colon. Your function needs to detect these headers and return the section name without the colon.

  • Open clinical_nlp.py in VS Code and add the following code:
def detect_section(line):
    # Strip leading/trailing whitespace from the input
    stripped = line.strip()

    # Empty lines are not section headers
    if not stripped:
        return None

    # Section headers end with a colon
    if stripped.endswith(":"):
        # Remove the colon and any extra whitespace
        potential_name = stripped[:-1].strip()
        # Validate: must be alphabetic/spaces and reasonably short
        if potential_name and all(c.isalpha() or c.isspace() for c in potential_name) and len(potential_name) < 50:
            return potential_name.upper()

    return None

What does this code do?

  • line.strip() removes whitespace so inputs like " MEDICATIONS: " still work.
  • The function checks if the cleaned line ends with a colon, which is the hallmark of a section header.
  • stripped[:-1] removes the trailing colon, giving you just the section name.
  • potential_name.upper() normalizes the result to uppercase, so "Medications:" and "MEDICATIONS:" both return "MEDICATIONS".
  • The all(c.isalpha() or c.isspace() ...) check prevents false positives on lines like "123:" or lines with special characters.
  • Save clinical_nlp.py.
  • You won't see a visible change yet. The next substep writes a test that proves this function works.
Write your first test

pytest discovers test files automatically by looking for files named with a test_ prefix. Inside those files, it runs any function whose name starts with test_.

  • Open test_clinical_nlp.py and add the following code:
import pytest
from clinical_nlp import detect_section


def test_detect_section_finds_medications():
    # Verify that a standard section header is detected correctly
    assert detect_section("MEDICATIONS:") == "MEDICATIONS"

What does this code do?

  • from clinical_nlp import detect_section pulls your function from the module you just wrote.
  • The test function name test_detect_section_finds_medications describes exactly what scenario it checks.
  • assert is Python's built-in keyword for stating "this must be true." If the expression evaluates to False, pytest reports a failure.
  • Save test_clinical_nlp.py.
  • Run your test suite by running this command in the terminal:
pytest
  • You should see output that includes 1 passed in green text.

Seeing 'ModuleNotFoundError'?

  • Make sure both clinical_nlp.py and test_clinical_nlp.py are in the same folder.
  • Check that the filename is exactly clinical_nlp.py with an underscore, not a hyphen.
  • Make sure you saved clinical_nlp.py before running pytest. Unsaved changes won't be picked up.

Why is pytest not finding my clinical_nlp module?

Make the test fail on purpose

Seeing a passing test is satisfying. But the real power of pytest shows up when something goes wrong.

By intentionally breaking your function, you'll learn to read pytest's failure output. This skill separates developers who stare at bugs for hours from those who fix them in minutes.

  • In clinical_nlp.py, find this line:
            return potential_name.upper()
  • Replace potential_name.upper() with stripped, so the line now looks like this:
            return stripped
  • Save clinical_nlp.py and run pytest again:
pytest

Your test now fails. Look at the output carefully.

What should I see?

pytest shows the test name that failed: test_detect_section_finds_medications.

Below that, it shows the assert line and the values on each side. You'll see something like AssertionError: assert 'MEDICATIONS:' == 'MEDICATIONS'. The left side is what your function actually returned. The right side is what you expected.

This is assertion introspection. pytest automatically shows you both values so you can pinpoint the mismatch instantly.

Why does this matter in healthcare?

In a real healthcare NLP pipeline, this kind of bug would mean section names include stray colons. Downstream code splitting notes by section would break silently, potentially hiding critical patient information.

Your test caught this in seconds.

Now fix the function back to its working state.

  • In clinical_nlp.py, find this line:
            return stripped
  • Replace stripped with potential_name.upper(), so the line now looks like this:
            return potential_name.upper()
  • Save clinical_nlp.py and run pytest one more time:
pytest
  • You should see 1 passed in green again.

The test-driven debugging loop

What you just did is the core debugging cycle: break something, read the failure, fix it, confirm green. Every time a test fails in the future, pytest hands you this same roadmap to the bug.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files and that pytest shows 1 passed when you run it.

ⓧ I'd like to double check the full code

Here's what each file should look like at this point.

def detect_section(line):
    stripped = line.strip()
    if not stripped:
        return None
    if stripped.endswith(":"):
        potential_name = stripped[:-1].strip()
        if potential_name and all(c.isalpha() or c.isspace() for c in potential_name) and len(potential_name) < 50:
            return potential_name.upper()
    return None
import pytest
from clinical_nlp import detect_section


def test_detect_section_finds_medications():
    assert detect_section("MEDICATIONS:") == "MEDICATIONS"

Your first test proves that detect_section works for the happy path. Next up, you'll test the messy edge cases that real clinical text throws at your function.

Test Edge Cases in Section Detection

Your first test confirmed that detect_section can find a clean, uppercase header like "MEDICATIONS:". Real clinical notes are messier than that.

Doctors and EHR systems don't always format headers consistently. You might see "Medications:" in mixed case, or " MEDICATIONS: " with extra whitespace. In this step, you'll write tests that prove your function handles these messy inputs correctly.

In this step, get ready to:
  • Write tests that verify your function handles mixed case and whitespace.
  • Write tests that confirm non-header text returns None.
  • Understand why edge case testing matters for healthcare AI safety.
Test mixed case and extra whitespace

Clinical notes come from many sources. A section header might arrive as "Medications:" instead of "MEDICATIONS:", or with stray spaces around it.

  • In test_clinical_nlp.py, add two new test functions below your existing test by copying this code:
def test_detect_section_case_insensitive():
    assert detect_section("Medications:") == "MEDICATIONS"


def test_detect_section_strips_whitespace():
    assert detect_section("  MEDICATIONS:  ") == "MEDICATIONS"

What do these tests check?

test_detect_section_case_insensitive verifies that "Medications:" (mixed case) still gets normalized to "MEDICATIONS". The function should treat headers as case-insensitive.

test_detect_section_strips_whitespace verifies that leading and trailing spaces don't break detection. Whitespace is common in text extracted from PDFs or EHR exports.

  • Save test_clinical_nlp.py.
  • Run your tests by entering this command in the terminal:
pytest
  • You should see 3 passed in green. All three tests pass because your function already normalizes case and strips whitespace.

Seeing a test failure?

Check that you placed the new test functions after the existing test_detect_section_finds_medications function, not inside it. Each def should be at the top level of the file (no indentation).

Make sure your assert lines match exactly, including the quotes and spaces inside the test strings.

Help me debug my failing edge case tests for detect_section.

Test inputs that should NOT be detected as sections

Catching headers is only half the job. Your function also needs to correctly reject lines that look like regular text. These are called negative test cases.

  • In test_clinical_nlp.py, add these two tests below your whitespace test:
def test_detect_section_returns_none_for_regular_text():
    assert detect_section("Patient takes aspirin daily.") is None


def test_detect_section_empty_string():
    assert detect_section("") is None

What do these tests check?

test_detect_section_returns_none_for_regular_text confirms that a normal sentence (no colon pattern) returns None. This prevents false positives where regular clinical text gets misidentified as a header.

test_detect_section_empty_string verifies the function handles empty input gracefully. Empty lines are common between sections in clinical notes.

  • Save test_clinical_nlp.py.
  • Run the full test suite by entering this command:
pytest -v
  • You should see all 5 tests listed individually, each showing PASSED next to its name. The -v flag gives you verbose output so you can see each test name and its result.

Tests not being discovered?

pytest discovers test functions by looking for the test_ prefix. Make sure each function name starts with test_ and is defined at the file's top level.

Also confirm your file is named test_clinical_nlp.py (with the test_ prefix). Files without this prefix are invisible to pytest.

Help me figure out why pytest isn't finding my tests.

Understand why edge cases matter in healthcare

You now have tests covering both positive cases (valid headers) and negative cases (regular text). This combination is what makes a test suite trustworthy.

Why do edge cases matter in clinical NLP?

Imagine a clinical note that contains the line "TAKE MEDICATIONS: as directed". A poorly written function might detect this as a section header and split the note in the wrong place.

That split could hide critical information from downstream systems. A medication reconciliation tool might never see the instructions that follow. Your negative tests prove this category of failure won't happen.

Why test for None specifically?

Using is None instead of == None is a Python best practice. The is keyword checks identity (the exact None object), while == checks equality (which could be overridden by custom classes). In safety-critical code, precision matters.

✔️ Awesome, I've got everything!

Great. Double check you've saved test_clinical_nlp.py and all 5 tests show PASSED when you run pytest -v.

ⓧ I'd like to double check the full code

Here is the complete test_clinical_nlp.py file at this point:

import pytest
from clinical_nlp import detect_section


def test_detect_section_finds_medications():
    assert detect_section("MEDICATIONS:") == "MEDICATIONS"


def test_detect_section_case_insensitive():
    assert detect_section("Medications:") == "MEDICATIONS"


def test_detect_section_strips_whitespace():
    assert detect_section("  MEDICATIONS:  ") == "MEDICATIONS"


def test_detect_section_returns_none_for_regular_text():
    assert detect_section("Patient takes aspirin daily.") is None


def test_detect_section_empty_string():
    assert detect_section("") is None

And the clinical_nlp.py file remains unchanged:

def detect_section(line):
    stripped = line.strip()
    if not stripped:
        return None
    if stripped.endswith(":"):
        potential_name = stripped[:-1].strip()
        if potential_name and all(c.isalpha() or c.isspace() for c in potential_name) and len(potential_name) < 50:
            return potential_name.upper()
    return None

You've built a solid foundation of edge case tests that prove your section detector handles messy real-world input. Next up, you'll write a brand new function that extracts medication names and dosages using regex, then put it through the same rigorous testing.

Write a Regex Extractor and Test It

Your section detection tests are green. Now it's time to tackle the harder problem: extracting structured data from messy clinical text.

In this step, you'll write a function that uses regex to pull medication names and dosages out of free-text clinical notes. Then you'll write tests that prove it handles both clean and tricky inputs.

In this step, get ready to:
  • Write a regex-based medication extraction function.
  • Write tests for successful extractions.
  • Write tests for tricky edge case inputs.
Write a medication extraction function

Clinical notes often contain medication information buried in free text like "Aspirin 81mg daily" or "Metformin 500 mg twice daily". Your function needs to find these patterns and pull out the drug name and dosage as structured data.

  • In clinical_nlp.py, add an import re statement at the very top of the file (above the detect_section function), then add the following function below detect_section:
import re


def extract_medications(text):
    """Extract medication names and dosages from clinical text.

    Looks for patterns like "Aspirin 81mg" or "Metformin 500 mg".
    Returns a list of (name, dosage) tuples.
    """
    pattern = r'([A-Z][a-z]+)\s+(\d+\s*mg)'
    return re.findall(pattern, text)

What does this code do?

The regex pattern r'([A-Z][a-z]+)\s+(\d+\s*mg)' does the heavy lifting:

  • ([A-Z][a-z]+) captures a capitalized word (the medication name).
  • \s+ matches one or more spaces between the name and dosage.
  • (\d+\s*mg) captures digits followed by an optional space and "mg" (the dosage).

re.findall() returns all non-overlapping matches as a list of tuples, where each tuple contains the captured groups.

  • Save clinical_nlp.py.

Seeing a syntax error?

Make sure the import re line is at the very top of the file, before any function definitions. Also check that the regex string uses a raw string prefix (r'...') to prevent Python from interpreting the backslashes.

help me fix a syntax error in my extract_medications function

✔️ Awesome, I've got everything!

Great. Make sure you've saved clinical_nlp.py before moving on.

ⓧ I'd like to double check the full code

import re


def detect_section(line):
    stripped = line.strip()
    if not stripped:
        return None
    if stripped.endswith(":"):
        potential_name = stripped[:-1].strip()
        if potential_name and all(c.isalpha() or c.isspace() for c in potential_name) and len(potential_name) < 50:
            return potential_name.upper()
    return None


def extract_medications(text):
    """Extract medication names and dosages from clinical text.

    Looks for patterns like "Aspirin 81mg" or "Metformin 500 mg".
    Returns a list of (name, dosage) tuples.
    """
    pattern = r'([A-Z][a-z]+)\s+(\d+\s*mg)'
    return re.findall(pattern, text)
Write tests for successful extraction

Now you need tests that prove your function can find medications in realistic clinical text. You'll test both a single medication and a block of text containing multiple medications.

  • In test_clinical_nlp.py, find the import line at the top of the file:
from clinical_nlp import detect_section
  • Update it to also import extract_medications, so the line now looks like this:
from clinical_nlp import detect_section, extract_medications
  • Add the following two test functions at the bottom of test_clinical_nlp.py:
def test_extract_medications_single():
    result = extract_medications("Aspirin 81mg")
    assert result == [("Aspirin", "81mg")]


def test_extract_medications_multiple():
    text = "Aspirin 81mg daily. Metformin 500mg twice daily."
    result = extract_medications(text)
    assert result == [("Aspirin", "81mg"), ("Metformin", "500mg")]

What do these tests verify?

test_extract_medications_single checks the simplest case: one medication with a dosage immediately after it. The expected result is a list containing one tuple.

test_extract_medications_multiple checks that the function finds all medications in a block of text, not just the first one. The expected result is a list of two tuples.

  • Save test_clinical_nlp.py and run your tests:
pytest

You should see 7 tests passing (5 from before, plus your 2 new ones).

Seeing an ImportError?

If you see an error about extract_medications not being found, make sure you saved clinical_nlp.py after adding the new function. The import in the test file pulls from the saved version on disk.

help me fix an ImportError for extract_medications in my test file

Write tests for tricky inputs

Real clinical notes are messy. Dosages might have a space between the number and "mg", or a note might contain no medications at all. Your tests need to verify the function handles these cases gracefully.

  • Add these three test functions at the bottom of test_clinical_nlp.py:
def test_extract_medications_with_space_in_dose():
    result = extract_medications("Metformin 500 mg")
    assert result == [("Metformin", "500 mg")]


def test_extract_medications_empty_text():
    assert extract_medications("") == []


def test_extract_medications_no_match():
    assert extract_medications("No medications listed.") == []

What do these edge case tests catch?

test_extract_medications_with_space_in_dose verifies the \s* in your regex works. Without it, "500 mg" (with a space) would be missed entirely.

test_extract_medications_empty_text and test_extract_medications_no_match verify the function returns an empty list instead of crashing or returning None. In a pipeline, downstream code expects a list it can iterate over.

  • Save test_clinical_nlp.py and run pytest in verbose mode to see every test name and its result:
pytest -v

You should see 10 tests listed individually, each with a PASSED status. Verbose mode shows the full test function name next to each result, making it easy to spot which specific scenario failed if something breaks later.

One of the new tests failing?

If test_extract_medications_with_space_in_dose fails, check that your regex pattern in clinical_nlp.py uses \s* (zero or more spaces) between \d+ and mg. The full pattern should be r'([A-Z][a-z]+)\s+(\d+\s*mg)'.

help me debug a failing medication extraction test

✔️ Awesome, I've got everything!

Great. Make sure you've saved test_clinical_nlp.py and all 10 tests are passing.

ⓧ I'd like to double check the full code

import pytest
from clinical_nlp import detect_section, extract_medications


def test_detect_section_finds_medications():
    assert detect_section("MEDICATIONS:") == "MEDICATIONS"


def test_detect_section_case_insensitive():
    assert detect_section("Medications:") == "MEDICATIONS"


def test_detect_section_strips_whitespace():
    assert detect_section("  MEDICATIONS:  ") == "MEDICATIONS"


def test_detect_section_returns_none_for_regular_text():
    assert detect_section("Patient takes aspirin daily.") is None


def test_detect_section_empty_string():
    assert detect_section("") is None


def test_extract_medications_single():
    result = extract_medications("Aspirin 81mg")
    assert result == [("Aspirin", "81mg")]


def test_extract_medications_multiple():
    text = "Aspirin 81mg daily. Metformin 500mg twice daily."
    result = extract_medications(text)
    assert result == [("Aspirin", "81mg"), ("Metformin", "500mg")]


def test_extract_medications_with_space_in_dose():
    result = extract_medications("Metformin 500 mg")
    assert result == [("Metformin", "500 mg")]


def test_extract_medications_empty_text():
    assert extract_medications("") == []


def test_extract_medications_no_match():
    assert extract_medications("No medications listed.") == []

You now have 10 tests covering both of your NLP functions. Next up, you'll deliberately break your code and use pytest's failure output to practice the test-driven debugging loop.

Use Failing Tests to Debug

You have 10 passing tests and two working functions. Everything is green. But in a real project, bugs sneak in through small code changes.

In this step, you'll intentionally break your code to see how pytest responds. The failure output tells you exactly which input cases broke and what the function returned instead of what you expected. This is the test-driven debugging loop: break, read, fix, confirm.

In this step, get ready to:
  • Introduce a deliberate bug by modifying your regex pattern.
  • Read pytest's failure output to identify what went wrong.
  • Fix the bug using clues from the test output.
Introduce a deliberate bug

Real bugs often come from small regex tweaks. You'll simulate this by removing the part of your pattern that handles spaces between a number and "mg".

  • In clinical_nlp.py, find the pattern line inside extract_medications:
pattern = r'([A-Z][a-z]+)\s+(\d+\s*mg)'
  • Remove \s* from the second capture group so the line now looks like this:
pattern = r'([A-Z][a-z]+)\s+(\d+mg)'

What did you just break?

The \s* in the original pattern means "zero or more whitespace characters." It allowed the regex to match both 500mg (no space) and 500 mg (with a space).

Without it, the pattern only matches when the number and "mg" are glued together. Any medication with a space in the dosage will silently disappear from the results.

  • Save clinical_nlp.py.
  • Run your test suite to see what breaks by running this command:
pytest

You should see 9 tests pass and 1 test fail. The failing test is test_extract_medications_with_space_in_dose.

Why did only one test fail?

Your other medication tests use inputs like "Aspirin 81mg" where there's no space between the number and "mg". Those still match the broken pattern.

Only "Metformin 500 mg" (with a space) fails. The targeted failure tells you the exact input format your regex can no longer handle.

Read the failure output like a detective

Pytest doesn't just say "test failed." It shows you exactly what went wrong through assertion introspection.

  • Look at the failure output in your terminal. You'll see something like this:
FAILED test_clinical_nlp.py::test_extract_medications_with_space_in_dose

    def test_extract_medications_with_space_in_dose():
        result = extract_medications("Metformin 500 mg")
>       assert result == [("Metformin", "500 mg")]
E       AssertionError: assert [] == [('Metformin', '500 mg')]

How to read this output

  • The test name (test_extract_medications_with_space_in_dose) tells you which input scenario failed.
  • The > arrow points to the exact line that failed.
  • The E line shows the assertion error: the actual value ([]) vs. the expected value ([("Metformin", "500 mg")]).
  • The empty list [] means the regex matched nothing at all for this input. That's your clue that the pattern can't handle a space.

Why this matters in healthcare

In a clinical pipeline, this bug would silently drop medications from patient summaries. The function wouldn't crash. It would just return fewer results, and no one would notice until a clinician missed a drug interaction.

Your test caught it immediately and told you exactly which format broke. That's the value of testing safety-critical code.

Fix the bug using the test output

The failing test told you that "Metformin 500 mg" (with a space between the number and "mg") returned an empty list. The fix is to allow optional whitespace in that position again.

  • In clinical_nlp.py, find the broken pattern line:
pattern = r'([A-Z][a-z]+)\s+(\d+mg)'
  • Add \s* back between \d+ and mg so the line looks like this:
pattern = r'([A-Z][a-z]+)\s+(\d+\s*mg)'
  • Save clinical_nlp.py.
  • Confirm all tests pass again by running this command:
pytest -v

You should see all 10 tests passing with green output. The -v flag shows each test name and its pass/fail status.

The test-driven debugging loop

You just completed the full cycle: break something, run the tests, read the failure output, fix the code, confirm green. This loop works the same whether the bug was intentional or accidental.

The key insight is that well-named tests act as a map. When test_extract_medications_with_space_in_dose fails, you already know the problem is with space-containing dosages before reading a single line of implementation code.

Still seeing a failure?

  • Make sure you placed \s* between \d+ and mg (not after "mg" or before \d+).
  • Check that you saved the file. Unsaved changes in your editor won't be picked up by pytest.
  • Verify the full pattern string is r'([A-Z][a-z]+)\s+(\d+\s*mg)' with no extra or missing characters.

Help me fix my regex pattern so all medication tests pass.

✔️ Awesome, I've got everything!

Great. Double check you've saved clinical_nlp.py with the restored pattern.

ⓧ I'd like to double check the full code

Here's clinical_nlp.py with the correct (restored) regex pattern:

import re


def detect_section(line):
    stripped = line.strip()
    if not stripped:
        return None
    if stripped.endswith(":"):
        potential_name = stripped[:-1].strip()
        if potential_name and all(c.isalpha() or c.isspace() for c in potential_name) and len(potential_name) < 50:
            return potential_name.upper()
    return None


def extract_medications(text):
    """Extract medication names and dosages from clinical text.

    Looks for patterns like "Aspirin 81mg" or "Metformin 500 mg".
    Returns a list of (name, dosage) tuples.
    """
    pattern = r'([A-Z][a-z]+)\s+(\d+\s*mg)'
    return re.findall(pattern, text)

And here's test_clinical_nlp.py (unchanged from the previous step):

import pytest
from clinical_nlp import detect_section, extract_medications


def test_detect_section_finds_medications():
    assert detect_section("MEDICATIONS:") == "MEDICATIONS"


def test_detect_section_case_insensitive():
    assert detect_section("Medications:") == "MEDICATIONS"


def test_detect_section_strips_whitespace():
    assert detect_section("  MEDICATIONS:  ") == "MEDICATIONS"


def test_detect_section_returns_none_for_regular_text():
    assert detect_section("Patient takes aspirin daily.") is None


def test_detect_section_empty_string():
    assert detect_section("") is None


def test_extract_medications_single():
    result = extract_medications("Aspirin 81mg")
    assert result == [("Aspirin", "81mg")]


def test_extract_medications_multiple():
    text = "Aspirin 81mg daily. Metformin 500mg twice daily."
    result = extract_medications(text)
    assert result == [("Aspirin", "81mg"), ("Metformin", "500mg")]


def test_extract_medications_with_space_in_dose():
    result = extract_medications("Metformin 500 mg")
    assert result == [("Metformin", "500 mg")]


def test_extract_medications_empty_text():
    assert extract_medications("") == []


def test_extract_medications_no_match():
    assert extract_medications("No medications listed.") == []

You've experienced the full debugging loop that professional developers use daily. Next up, you'll add input validation to your functions and write tests that verify exceptions are raised correctly.

Test Exception Handling

In the previous step, you used failing tests to track down a regex bug. You saw how pytest's output pinpoints exactly which inputs break your code.

But what happens when someone passes None or an integer to your functions instead of a string? Right now, your functions crash with a confusing AttributeError. In this step, you'll add input validation that raises clear errors, then write tests to prove that safety net works.

In this step, get ready to:
  • Add type checking to both NLP functions so they fail loudly on bad input.
  • Write tests that verify exceptions are raised correctly.
  • Run the full 12-test suite and confirm everything passes.
Add input validation to your functions

In a healthcare pipeline, your NLP functions might receive data from upstream systems that send unexpected types. If detect_section receives None from a database query, it should raise a clear TypeError instead of crashing with a cryptic error three layers deep.

  • In clinical_nlp.py, replace your entire detect_section function with this updated version that includes a docstring and type check:
def detect_section(line):
    """Detect if a line is a section header in a clinical note.
    
    Section headers are lines that end with a colon and contain
    primarily uppercase letters (e.g., "MEDICATIONS:", "ASSESSMENT:").
    
    Returns the section name (without colon) or None.
    """
    # Reject non-string input immediately
    if not isinstance(line, str):
        raise TypeError("Input must be a string")
    
    stripped = line.strip()
    
    if not stripped:
        return None
    
    if stripped.endswith(":"):
        potential_name = stripped[:-1].strip()
        if potential_name and all(c.isalpha() or c.isspace() for c in potential_name) and len(potential_name) < 50:
            return potential_name.upper()
    
    return None

What does this code do?

The if not isinstance(line, str) check runs before anything else in the function. If the input is not a string, it raises a TypeError with a clear message.

This is called "failing loudly." Instead of letting Python crash on line.strip() with a confusing AttributeError, your function tells the caller exactly what went wrong.

You won't see a change yet. The next chunk updates the second function, and then you'll write tests to verify both.

  • Now replace your entire extract_medications function with this version that adds the same type check:
def extract_medications(text):
    """Extract medication names and dosages from clinical text.
    
    Looks for patterns like "Aspirin 81mg" or "Metformin 500 mg".
    Returns a list of (name, dosage) tuples.
    """
    # Reject non-string input immediately
    if not isinstance(text, str):
        raise TypeError("Input must be a string")
    
    pattern = r'([A-Z][a-z]+)\s+(\d+\s*mg)'
    return re.findall(pattern, text)

What does this code do?

The same pattern applies here. The isinstance(text, str) check guards against someone passing an integer, a list, or None into the medication extractor.

Without this guard, passing 123 would cause re.findall to raise a TypeError deep inside the regex engine. Your explicit check makes the error source obvious.

  • Save clinical_nlp.py.

You won't see a visible change yet. The next substep writes tests that exercise these new guards.

✔️ 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 re


def detect_section(line):
    """Detect if a line is a section header in a clinical note.
    
    Section headers are lines that end with a colon and contain
    primarily uppercase letters (e.g., "MEDICATIONS:", "ASSESSMENT:").
    
    Returns the section name (without colon) or None.
    """
    if not isinstance(line, str):
        raise TypeError("Input must be a string")
    
    stripped = line.strip()
    
    if not stripped:
        return None
    
    if stripped.endswith(":"):
        potential_name = stripped[:-1].strip()
        if potential_name and all(c.isalpha() or c.isspace() for c in potential_name) and len(potential_name) < 50:
            return potential_name.upper()
    
    return None


def extract_medications(text):
    """Extract medication names and dosages from clinical text.
    
    Looks for patterns like "Aspirin 81mg" or "Metformin 500 mg".
    Returns a list of (name, dosage) tuples.
    """
    if not isinstance(text, str):
        raise TypeError("Input must be a string")
    
    pattern = r'([A-Z][a-z]+)\s+(\d+\s*mg)'
    return re.findall(pattern, text)
Write tests that verify exceptions are raised

pytest provides pytest.raises() as a context manager for testing that code raises a specific exception. If the exception is raised inside the with block, the test passes. If no exception is raised, the test fails.

  • In test_clinical_nlp.py, add a section comment above your first test function (test_detect_section_finds_medications), then add the new exception test after test_detect_section_empty_string. Copy this code:
# --- Section Detection Tests ---

# ... your existing section detection tests stay here ...


def test_detect_section_raises_typeerror_for_none():
    # Verify that passing None raises a TypeError
    with pytest.raises(TypeError):
        detect_section(None)

What does pytest.raises do?

The with pytest.raises(TypeError): block tells pytest: "the code inside MUST raise a TypeError." If it does, the test passes. If it doesn't raise anything (or raises a different exception), the test fails.

This proves your code fails loudly with the right error type, rather than silently returning garbage data that could end up in a patient record.

  • Now add a section comment above test_extract_medications_single, then add the second exception test at the very end of the file:
# --- Medication Extraction Tests ---

# ... your existing medication extraction tests stay here ...


def test_extract_medications_raises_typeerror_for_int():
    # Verify that passing an integer raises a TypeError
    with pytest.raises(TypeError):
        extract_medications(123)

What does this test prove?

This test passes 123 (an integer) to extract_medications. Your function's isinstance check catches the wrong type and raises TypeError before the regex ever runs.

  • Save test_clinical_nlp.py.

Seeing an import error for pytest.raises?

Make sure you have import pytest at the top of your test file. The pytest.raises context manager comes from the pytest package itself.

help me fix my pytest.raises import

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before running the tests.

ⓧ I'd like to double check the full code

import pytest
from clinical_nlp import detect_section, extract_medications


# --- Section Detection Tests ---

def test_detect_section_finds_medications():
    assert detect_section("MEDICATIONS:") == "MEDICATIONS"


def test_detect_section_case_insensitive():
    assert detect_section("Medications:") == "MEDICATIONS"


def test_detect_section_strips_whitespace():
    assert detect_section("  MEDICATIONS:  ") == "MEDICATIONS"


def test_detect_section_returns_none_for_regular_text():
    assert detect_section("Patient takes aspirin daily.") is None


def test_detect_section_empty_string():
    assert detect_section("") is None


def test_detect_section_raises_typeerror_for_none():
    with pytest.raises(TypeError):
        detect_section(None)


# --- Medication Extraction Tests ---

def test_extract_medications_single():
    result = extract_medications("Aspirin 81mg")
    assert result == [("Aspirin", "81mg")]


def test_extract_medications_multiple():
    text = "Aspirin 81mg daily. Metformin 500mg twice daily."
    result = extract_medications(text)
    assert result == [("Aspirin", "81mg"), ("Metformin", "500mg")]


def test_extract_medications_with_space_in_dose():
    result = extract_medications("Metformin 500 mg")
    assert result == [("Metformin", "500 mg")]


def test_extract_medications_empty_text():
    assert extract_medications("") == []


def test_extract_medications_no_match():
    assert extract_medications("No medications listed.") == []


def test_extract_medications_raises_typeerror_for_int():
    with pytest.raises(TypeError):
        extract_medications(123)
Run the full test suite

You now have 12 tests covering normal cases, edge cases, and exception handling. Time to run them all and see the full picture.

  • Run your full test suite in verbose mode by running this command:
pytest -v
  • You should see all 12 tests listed by name, each with a PASSED label next to it.

What should I see?

The -v flag shows verbose output. Each test appears on its own line with its full name and result. At the bottom, you'll see a summary like 12 passed.

Notice the pattern across all your tests. Each test is small, tests one thing, and has a clear name that describes the scenario it covers.

Seeing failures?

If test_detect_section_raises_typeerror_for_none fails, make sure you added the if not isinstance(line, str) check at the very top of your detect_section function, before line.strip().

If other tests that were passing before now fail, double check that you didn't accidentally delete any lines when replacing your functions.

help me debug my failing exception tests

Why does defensive programming matter in healthcare?

In a real clinical pipeline, data flows through multiple systems. A database query might return None for a missing field. A JSON parser might hand you an integer where you expected a string.

Without type checks, these errors propagate silently. Your function returns an empty list instead of raising an alarm. With type checks and tests proving them, you have a contract: bad input gets caught immediately, not three functions later.

Secret mission

Parametrize Your Tests

Right now, each test case gets its own function. That works, but it doesn't scale. What if you needed to test 50 clinical note snippets? You'd be writing 50 nearly identical functions. In this mission, you'll use pytest's parametrize decorator to run one test function with many different inputs, and extend your extractor to handle mcg dosages.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for reference, or delete them to free up space.

Resources you used:

  • The healthcare-nlp-tests project folder on your Desktop (contains clinical_nlp.py and test_clinical_nlp.py).
  • The pytest package installed via pip.

Keep everything running

No action needed. Choose this if you plan to keep building on your test suite or want to reference it later.

  • Your project folder stays on your Desktop.
  • The pytest package remains installed and available for future projects.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files stay on disk until you delete them.

  • Close VS Code to free up memory.
  • Your project folder and installed packages remain ready for when you come back.

Delete - I don't want to use this again

Remove all project files and the pytest package to start fresh.

  • Delete the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/healthcare-nlp-tests

Windows

rmdir /s /q %USERPROFILE%\Desktop\healthcare-nlp-tests
  • Uninstall pytest by running:
pip uninstall pytest
  • Confirm the removal when prompted by typing y.

Nice Work!

Nice Work!

You just built a test suite that guards healthcare NLP code against dangerous bugs. Your functions now have a safety net that catches silent failures before they ever reach a patient record.

You've learned how to:

  • Write unit tests with pytest that validate NLP text processing functions using assert statements and pytest.raises().
  • Use pytest's assertion introspection to read failure output like a detective and pinpoint exactly where a bug lives in your code.
  • Test edge cases and exception handling to prevent silent failures in safety-critical healthcare AI pipelines.
  • Used @pytest.mark.parametrize to run one test function against a batch of 10 real-world-style clinical note snippets.

Ready to quiz yourself?