Type hints make Python code self-documenting and let static type checkers catch bugs before runtime. pytest is the standard testing framework. Together they form the safety net that lets you refactor production code with confidence. This lesson covers the hint syntax, mypy, and the pytest features you will use every day.
Basic type hints
Add hints to function parameters and the return value with the colon and arrow syntax. The runtime ignores them, so they are zero-cost documentation. Editors use them for autocomplete and inline errors.
def greet(name: str, excited: bool = False) -> str:
suffix = "!" if excited else "."
return f"Hello, {name}{suffix}"
print(greet("Asha", excited=True)) # Hello, Asha!
Hints for collections
Use list[int], dict[str, int], tuple[int, str, float] for container types. Python 3.9 made these work directly. For 3.8 and earlier you must import from typing (List[int], Dict[str, int]).
from typing import Optional
def top_n(scores: list[int], n: int = 3) -> list[int]:
return sorted(scores, reverse=True)[:n]
print(top_n([42, 17, 99, 8, 63], n=2)) # [99, 63]
# Optional means the value can be the type or None
def find_user(user_id: int) -> Optional[dict]:
return {"id": user_id, "name": "Asha"} if user_id > 0 else None
Type aliases and the new type statement
A type alias gives a name to a complex type. Python 3.12 added the type statement for cleaner alias syntax. Use TypeAlias when targeting 3.11 and earlier.
# Python 3.12+
type UserId = int
type UserMap = dict[UserId, str]
def lookup(users: UserMap, user_id: UserId) -> str:
return users.get(user_id, "unknown")
print(lookup({1: "Asha", 2: "Bala"}, 1)) # Asha
Run mypy for static type checking
mypy reads your code without running it and reports type errors. Install it in your dev venv and run it before commit. Pair it with pre-commit for automatic checks on every save.
(.venv) $ pip install mypy
(.venv) $ mypy your_module.py
Success: no issues found in 1 source file
Your first pytest
pytest discovers test files matching test_*.py and test functions matching test_*. A test is just a function that asserts something. Run pytest from the project root and it finds everything.
# test_math_helpers.py
def add(a: int, b: int) -> int:
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-2, -3) == -5
(.venv) $ pytest
====== test session starts ======
collected 2 items
test_math_helpers.py .. [100%]
====== 2 passed in 0.01s ======
Fixtures for shared setup
A fixture provides test data or resources. pytest calls the fixture function and passes its return value into any test that lists it as a parameter. Use a finalizer to clean up resources like database connections.
import pytest
@pytest.fixture
def sample_users():
return [{"id": 1, "name": "Asha"}, {"id": 2, "name": "Bala"}]
def test_user_count(sample_users):
assert len(sample_users) == 2
def test_user_name(sample_users):
assert sample_users[0]["name"] == "Asha"
Parametrize to run one test with many inputs
pytest.mark.parametrize runs a test once per input row. This is the cleanest way to test a function across many cases without writing a loop or copy-pasting assertions.
import pytest
def is_palindrome(s: str) -> bool:
return s == s[::-1]
@pytest.mark.parametrize("text,expected", [
("racecar", True),
("level", True),
("python", False),
("", True),
])
def test_palindrome(text, expected):
assert is_palindrome(text) is expected
Common pitfalls with typing and pytest
Three mistakes appear over and over. First, overusing Any defeats the purpose of type hints. If you find yourself reaching for Any, either define a Protocol that captures the structural shape you need, or split the function so the unknown part is small and explicit. Second, Optional[Something] without a None check at runtime is a silent crash waiting to happen. The hint tells mypy that None is allowed, but Python does not enforce it. Always pair Optional with an explicit if value is None: raise or return default. Third, tests that depend on filesystem ordering or time-of-day flake out in CI. Inject a clock (freezegun or dependency injection) and use tmp_path from pytest instead of /tmp. A test that needs the outside world to behave is a test that breaks when the outside world does.
Comparison with TypeScript and JUnit
If you come from TypeScript, Python type hints feel like a stripped-down version of the same idea. TypeScript checks every file by default and erases types at compile time. Python type hints are completely optional and erased at runtime; mypy is a separate static analyser you opt into. TypeScript structural typing matches Protocol typing in Python (both accept any object with the right shape), but Python's nominal typing for classes still dominates. JUnit fans will feel at home with pytest fixtures: @pytest.fixture is the spiritual successor to @BeforeEach, with the difference that fixtures are injected by parameter name. pytest's parametrize is more concise than JUnit's @ParameterizedTest with arguments source, and pytest's plain assert statements read more naturally than JUnit's assertEquals + expected + actual.
Best practices for typed and tested Python in 2026
Three habits keep a typed, tested codebase maintainable. First, run mypy in strict mode (--strict) from the start. Adding hints to a large existing codebase is hard, but adding hints to a small codebase is easy. Start strict and the rest of the code will conform. Second, write the test before the bug fix. When a bug arrives in production, write a failing test that reproduces it, then make the test pass. The test stays in the suite forever and prevents regressions. Fourth, keep tests fast: under a second for unit tests, under a minute for integration tests. Use pytest -x to stop on first failure during development, and pytest --co to see what would run before kicking off the full suite.
Summary
Type hints document intent. mypy catches type errors before runtime. pytest is the standard test framework. Use fixtures for shared setup and parametrize to run one test with many inputs. Type hints plus tests give you the safety net to refactor with confidence. The next lesson covers common interview patterns and a small end-to-end project to pull everything together.