Python Intermediate Python Projects

Build multi-file Python projects that separate parsing, logic and output, store data safely, test with temporary files and run from a clean checkout.

Lesson 105 of 107Intermediate13 min read

Intermediate Python projects combine several skills into a program that reads real data, applies rules, saves results and can be run by someone else. The step up from beginner projects isn't harder syntax; it's structure: separate modules with clear responsibilities, persistent data with a defined format, automated tests and documented set-up.

Python
def remaining_stock(stock, checked_out):
    return {item: qty - checked_out.get(item, 0) for item, qty in stock.items()}

print(remaining_stock({"tripods": 8, "lamps": 6}, {"tripods": 3}))
# {'tripods': 5, 'lamps': 6}

This page suggests three projects, an equipment loan report for a photography club, a validated donation importer and a logged API client, and covers the structure each one needs. It draws on file handling, CSV, pytest, logging and virtual environments.

Module boundaries

Split the program by responsibility, so each part can be understood, tested and changed on its own:

Text
equipment_report/
    loans/
        __init__.py
        parsing.py      # read CSV files -> lists of dicts (no calculations)
        stock.py        # pure calculations on Python data (no files)
        report.py       # format results as text (no files, no calculations)
        cli.py          # glue: read arguments, call the others, write output
    tests/
        test_stock.py
        test_parsing.py
    pyproject.toml
    README.md

The calculation module, stock.py, takes plain dictionaries and returns plain dictionaries: it doesn't know whether data came from a CSV file, a database or a test. That makes it trivial to test and reuse. Parsing deals with messy input; reporting deals with presentation; the command-line module wires them together.

One large function that reads a file, calculates, formats and prints is hard to debug: when a total is wrong, you can't tell whether parsing, arithmetic or formatting is at fault. Separate modules localise failures.

Persistent data

Files outlive the program, so decide their format, encoding and failure policy up front:

  • Format: CSV for tables people open in spreadsheets; JSON for nested settings and state; SQLite (sqlite3) when you need queries or several record types.
  • Encoding: always encoding="utf-8", with newline="" for CSV.
  • Failure policy: what happens with a missing file, a malformed row or a crash halfway through writing?

Never write directly over your only copy of important data. Write to a temporary file and replace the original only when writing succeeded:

Python
import json, os
from pathlib import Path

def save_json(path, data):
    path = Path(path)
    tmp = path.with_suffix(".tmp")
    tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
    os.replace(tmp, path)       # atomic on the same file system

If the program crashes while writing, the original file is untouched. Keep a sample data file in the repository, with fake data, so others can run the project immediately.

Testing collaborators

Test pure logic directly with ordinary values, and test file-handling code with temporary files that the test creates and cleans up:

Python
# tests/test_stock.py
from loans.stock import remaining_stock

def test_items_not_checked_out_keep_full_stock():
    assert remaining_stock({"lamps": 6}, {}) == {"lamps": 6}

def test_parse_skips_bad_rows(tmp_path):
    file = tmp_path / "loans.csv"
    file.write_text("item,qty\ntripods,3\nlamps,many\n", encoding="utf-8")
    rows, errors = parse_loans(file)
    assert rows == [{"item": "tripods", "qty": 3}] and len(errors) == 1

pytest's tmp_path fixture provides a fresh folder for each test. Tests must never depend on files in your home folder or on data only you have; another developer, or continuous integration, must be able to run them on a clean machine.

Focus tests on the risky cases: missing equipment, items returned twice, empty files, malformed rows and quantities that would go negative. The mocking and fixtures lesson covers replacing network calls in the API client project.

Packaging and usage

A project is only finished when someone else can run it. Document and automate the set-up:

Text
## Setup
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
python -m pip install -e ".[dev]"

## Run
python -m loans.cli data/sample_loans.csv

## Test
python -m pytest

Declare dependencies in pyproject.toml (see dependency management), add a .gitignore for .venv and generated output, and include sample data.

The real test is a clean checkout: clone the repository into a new folder, follow the README exactly, and check that tests pass and the report matches the documented example. Anything you had to do that isn't written down, such as installing a package by hand or creating a folder, is a missing step. A project that only works in your personal environment isn't finished.

Independent practice and a worked check

Task: build an equipment report, a validated donation importer or a logged API client with modules, persistence and tests. Suggested milestones for the donation importer:

  1. parsing.py: read a CSV of donations, returning valid rows and a list of errors with line numbers.
  2. summary.py: total donations per month from clean rows (pure function).
  3. storage.py: save the summary to JSON with the temporary-file pattern.
  4. tests/: test parsing with tmp_path, the summary with plain data, and saving with a temporary folder.
  5. README.md: set-up, run and test commands, verified from a fresh clone.
Python
def monthly_totals(rows):
    totals = {}
    for row in rows:
        month = row["date"][:7]                 # "2026-03-14" -> "2026-03"
        totals[month] = totals.get(month, 0) + row["amount"]
    return totals

print(monthly_totals([{"date": "2026-03-14", "amount": 20}, {"date": "2026-03-20", "amount": 15},
                      {"date": "2026-04-02", "amount": 40}]))   # {'2026-03': 35, '2026-04': 40}

For the worked check, the responsibility that should be separated from stock arithmetic is external file parsing.

Reference and next practice

The Python Packaging User Guide's packaging tutorial explains project layout and pyproject.toml. Continue with advanced projects and revisit pytest in depth. Practise with the Python exercises and Python quiz.

Intermediate Python Projects Summary

  • Module boundaries: Separate parsing, validation, business logic and presentation into understandable responsibilities.
  • Persistent data: Files require a chosen schema, encoding and failure policy.
  • Testing collaborators: Test pure logic directly and control external file dependencies with temporary data.
  • Packaging and usage: Document the environment, dependencies and commands needed to run the project.

Intermediate Python Projects Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Intermediate Python Projects in a small program

Python
def remaining_stock(stock, checked_out):
    return {item: quantity - checked_out.get(item, 0) for item, quantity in stock.items()}

print(remaining_stock({"tripods": 8, "lamps": 6}, {"tripods": 3}))
Output
{'tripods': 5, 'lamps': 6}
Line-by-line explanation
  1. Lines 1 to 2A pure function: two dictionaries in, a new dictionary out, no files or printing.
  2. Line 2checked_out.get(item, 0) treats items with no loans as fully in stock.
  3. Line 4Three tripods are out, leaving five; lamps are untouched.
Example 2

Parsing, logic and reporting kept separate

Python
import csv, io

# parsing.py: text in, clean records and errors out
def parse_loans(text):
    rows, errors = [], []
    for line_no, row in enumerate(csv.DictReader(io.StringIO(text)), start=2):
        try:
            qty = int(row["qty"])
            if qty <= 0:
                raise ValueError("quantity must be positive")
            rows.append({"item": row["item"].strip().lower(), "qty": qty})
        except (ValueError, KeyError) as error:
            errors.append(f"line {line_no}: {error}")
    return rows, errors

# stock.py: pure calculation
def remaining_stock(stock, loans):
    out = dict(stock)
    for loan in loans:
        out[loan["item"]] = out.get(loan["item"], 0) - loan["qty"]
    return out

# report.py: formatting only
def format_report(remaining):
    return "\n".join(f"{item:<9}{qty:>3}{'  OVERDRAWN' if qty < 0 else ''}"
                     for item, qty in sorted(remaining.items()))

loans_csv = "item,qty\nTripods,3\nlamps,many\nlenses,2\ntripods,6\n"
loans, problems = parse_loans(loans_csv)
print(format_report(remaining_stock({"tripods": 8, "lamps": 6, "lenses": 4}, loans)))
print(problems)
Output
lamps      6
lenses     2
tripods   -1  OVERDRAWN
["line 3: invalid literal for int() with base 10: 'many'"]
Line-by-line explanation
  1. Lines 4 to 14Parsing normalises names, converts quantities and records bad rows with line numbers.
  2. Lines 17 to 21The calculation works on clean Python data and knows nothing about CSV.
  3. Lines 24 to 26Formatting flags negative stock rather than hiding it, which is a business rule worth testing.

Common Intermediate Python Projects Mistakes

Mixing file access into the calculation

Don't
def remaining_stock(path):
    with open(path) as f:                 # parsing, calculation and output in one place
        ...
        print(total)
Do
def remaining_stock(stock, loans):        # pure: easy to test and reuse
    ...
    return remaining

Overwriting the only data file directly

Don't
with open("loans.json", "w") as f:        # a crash here leaves an empty file
    json.dump(data, f)
Do
tmp = Path("loans.tmp")
tmp.write_text(json.dumps(data), encoding="utf-8")
os.replace(tmp, "loans.json")

Intermediate Python Projects in Real Projects

Intermediate projects and real work

This is the shape of most internal tools and automation scripts in organisations: import a spreadsheet export, apply business rules, produce a report or update another system, log what happened and run on a schedule. Projects structured this way are easy for colleagues to review and extend.

For a portfolio, one well-structured intermediate project with tests, a README and sample data is more convincing than several single-file scripts, because it shows the habits teams rely on: separation of concerns, safe data handling and reproducible set-up.

Intermediate Python Projects FAQ

What makes a Python project intermediate rather than beginner?

Structure rather than syntax: several modules with separate responsibilities, data stored in files or a database with a defined format, error handling for bad input, automated tests and documented set-up that works on another machine.

What are good intermediate Python project ideas?

An expense or inventory report from CSV files, a data importer with validation and error reports, a command-line tool for organising files, a small Flask API backed by SQLite, a web scraper that respects robots.txt and saves to JSON, or an API client with logging and retries.

How should I structure a Python project with several files?

Group code in a package folder with modules for parsing, core logic, storage and the command-line entry point, plus a tests/ folder, pyproject.toml, README.md and .gitignore. Keep core logic free of file and network access so it's easy to test.

Do intermediate projects need tests?

Yes. Tests are what let you change and extend a project confidently, and they show others that it works. Start with the pure logic, then add tests for parsing and storage using temporary files.

How do I check that someone else can run my project?

Clone it into a fresh folder, create a new virtual environment and follow your README exactly. If you have to do anything not written down, add it. Running the tests in continuous integration automates this check.

Intermediate Python Projects Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Which responsibility should be separated from stock arithmetic?

Multiple choicemedium

Which review concern is valid for intermediate python projects?

Intermediate Python Projects Quiz

2 questions to check what you learned.

  1. Which responsibility should be separated from stock arithmetic?

    • The meaning of subtraction
    • External file parsing
    • Python ignores the distinction
  2. Which statement correctly describes testing collaborators?

    • Test pure logic directly and control external file dependencies with temporary data.
    • Only the example output matters, not the rule
    • Every operation has the same behavior