Skip to content

Python

Personal lookup for things I always forget. Terse by design — just enough to jog memory.


pytest

Always run pytest via python -m pytest, not bare pytest

# Potentially wrong — uses system pytest, which can't find your project's modules:
pytest tests/test_foo.py -v   # ModuleNotFoundError

# Probably more right — uses the venv's pytest and adds the project root to sys.path:
python -m pytest tests/test_foo.py -v

Same reason as python -m for scripts: the current directory gets added to the module search path, so imports like from engine.foo import bar resolve correctly.

Share an object across all tests in a file (instantiate once)

@pytest.fixture(scope="module")
def scenario():
    return MyScenario(config)

scope="module" = created once per file. Default (no scope) = recreated for every test function.

Parametrize — one test case per item in a list

@pytest.mark.parametrize("x,y", [(1, 2), (3, 4)], ids=["case_a", "case_b"])
def test_something(x, y):
    ...

pytest runs test_something once per tuple — so this produces two test cases: test_something[case_a] with x=1, y=2 and test_something[case_b] with x=3, y=4. Each appears as a separate pass/fail line in pytest -v. IDs control the display name; omit them and pytest auto-generates names like test_something[1-2].

Fixtures are just functions passed as test parameters

@pytest.fixture
def my_data():
    return {"key": "value"}   # returned value is injected into the test

def test_foo(my_data):         # pytest sees the name and injects it
    assert my_data["key"] == "value"

Testing concepts

Integration test vs regression test

These are about why you're testing, not how:

  • Integration test: verifies that multiple components work correctly together — the emphasis is on the connections between parts. E.g. "does the validation step feed into the calculation step, which correctly produces a CSV."
  • Regression test: verifies that something that used to work still works after a change — the emphasis is on catching accidental breakage. You establish known-correct values, and the test alerts you if a future change causes them to drift.

The same test can be both. A test that runs the full scenario pipeline (integration) and compares output against expected values (regression) is serving both purposes — but "regression test" is the more useful description for what it does day-to-day.


Iteration

First item matching a condition (with default if none)

result = next((x for x in items if x > 0), None)

The second argument to next() is the default — without it, raises StopIteration.


Classes / OOP

Abstract base class — force subclasses to implement a method

from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def calculate(self):
        ...

Instantiating a subclass that doesn't implement all @abstractmethod methods raises TypeError.


Type hints

List of dicts

from typing import Any, Dict, List
def foo(items: List[Dict[str, Any]]) -> None:

Strings / formatting

f-string with number formatting

f"{value:,.2f}"    # 1,234,567.89  (comma thousands, 2 decimal places)
f"{ratio:.1%}"     # 12.3%         (percentage, 1 decimal)
f"{ratio:.2%}"     # 12.34%

Files / paths

Path relative to the current file (not the working directory)

from pathlib import Path
HERE = Path(__file__).parent
config = HERE / "configs" / "my_config.json"

__file__ is always the path to the Python file being executed — it doesn't change. The "working directory" is wherever your terminal is when you run the script — it does change.

Example of why this matters:

backend/
  tests/
    test_foo.py          ← wants to open tests/fixtures/data.json
    fixtures/
      data.json

If you run pytest from backend/, the working directory is backend/. open("fixtures/data.json") fails — there's no fixtures/ directly inside backend/. open("tests/fixtures/data.json") works today but breaks if you ever run from elsewhere. Path(__file__).parent / "fixtures" / "data.json" always works because it's anchored to where test_foo.py lives, not where you happen to be standing in the terminal.

Running a script that imports from your own project

# Wrong — Python doesn't know where to find your project's modules:
python tests/generate_fixture.py   # ModuleNotFoundError

# Right — run as a module from the project root:
python -m tests.generate_fixture   # works

python -m adds the current directory to the module search path. Always run from the project root (the directory that contains your top-level packages).

Read a JSON file

import json
with open(path) as f:
    data = json.load(f)