Testing Python Scripts With pytest
Python scripts often start small. That is fine. The problem is when a small script becomes important and still has no tests. Maybe it syncs Notion content. Maybe it builds a static site. Maybe it processes images. Maybe it imports customer data. At that point, a silent bug can waste real time. You do not need a huge test suite for every script. You do need focused tests around the behavior that matters.
The Mental Model
Testing scripts is easier when most of the logic is not trapped inside the command line entry point. Instead of one giant script like:
import sys
path = sys.argv[1]
# read files
# parse data
# call API
# write output
Prefer small functions:
def parse_article(path):
...
def build_payload(article):
...
def write_output(article, output_dir):
...
Then the command line wrapper can stay thin. The functions are easier to test.
Start With Pure Functions
Pure functions are the easiest to test. A pure function takes input and returns output without touching files, network, or global state. Example:
def slugify(title):
return title.lower().replace(" ", "-")
A pytest test:
def test_slugify():
assert slugify("Hello World") == "hello-world"
Real slug functions need more edge cases, but the pattern is simple. Start testing the parts that can be tested without setup.
Test File Behavior With `tmp_path`
Scripts often read and write files. pytest gives you `tmp_path`, a temporary directory for a test. Example:
def test_reads_markdown_title(tmp_path):
article_path = tmp_path / "article.md"
article_path.write_text("# Hello\n\nBody text")
article = read_article(article_path)
assert article.title == "Hello"
This avoids writing test files into your real project. Each test gets a clean temporary place to work. That is useful for static site builders, import scripts, exporters, and local automation.
Keep Network Calls at the Boundary
Network calls make tests slower and more fragile. If a script talks to an API, separate payload creation from API sending. For example:
def build_notion_payload(article):
...
def create_notion_page(client, payload):
...
You can test `buildnotionpayload` without calling Notion. Then keep a smaller number of integration checks for the real API when needed. Most bugs are often in parsing, mapping, formatting, and validation, not in the actual HTTP request line.
Test the Regression
When you fix a bug, write a test that would have caught it. For example, if an `.env` parser failed when a Notion URL wrapped onto the next line, create a test fixture for that case. The test does not need to cover every possible environment file. It needs to protect the behavior that broke. Regression tests are valuable because they come from real problems. They prevent the same bug from returning later.
Common Mistakes
Mistake 1: Testing only the command line
End-to-end CLI tests can be useful, but they are often harder to write. Put logic in functions so most behavior can be tested directly.
Mistake 2: Calling real APIs in every test
Real API calls make tests slow and dependent on credentials. Test request-building separately from sending.
Mistake 3: Writing broad tests with unclear failures
A good test should fail near the problem. If one giant test covers everything, debugging the failure is harder.
Where This Shows Up in Real Projects
pytest is useful for many project scripts:
- static site builds
- Markdown parsing
- Notion sync
- image asset processing
- import and export tools
- deployment helpers
- data cleanup scripts Any script that could break a build or change important data deserves at least a few focused tests.
Key Takeaways
- Put script logic in testable functions.
- Start with pure functions.
- Use `tmp_path` for file-based tests.
- Keep network calls at the boundary.
- Add regression tests for real bugs.
A small focused test suite is better than no tests for important scripts.
Related Articles
How to Structure a Python Project
- Python Virtual Environments Explained
- Debugging Broken Local Dev Environments