Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Frameworks & Tooling  ›  Lesson

The Static Typing Ecosystem

Static Typing 15 mintyping hints are unenforced at runtime; mypy/pyright check before running, Pydantic checks while running
Your task
Write type_check(args). It checks a fixed function, safe_divide(a: int, b: int) -> float, against the two values in args (args[0] is a, args[1] is b). Use safe_divide.__annotations__ and safe_divide.__code__.co_varnames to find each parameter's expected type, and isinstance to check the matching value. Return True only if every value matches its hint. Example: type_check([10, 2]) -> True, type_check([10, "2"]) -> False.

The Static Typing Ecosystem

In short: def greet(name: str, times: int) -> str: is a type hint — Python writes it down, stores it in fn.__annotations__, and then does nothing else with it. A hint never stops a call, never converts a value, never raises on its own. Catching a mismatch before the program runs is a separate tool's job: mypy or pyright read your hints and your calls and report a mismatch without running anything. Pydantic takes the opposite approach — it checks types at runtime, when real data (JSON from an API, a form submission) arrives untyped. typing.get_type_hints(fn) and fn.__annotations__ both run in the browser; mypy, pyright and Pydantic are read-only here, since none of them can run inside Pyodide.

greet(name: str, times: int) called as greet("hi", "3"): the runtime lane waves the mismatched call straight through the hint and only fails later, deep inside, with a TypeError about multiplying strings; the mypy/pyright lane stops the same call before it runs, with "expected int, got str"

Hint a function's shape

A hint documents what a function expects, for a human reading it and for a tool checking it — never for Python itself:

python

Both show the same three entries: name, times, and return. typing adds vocabulary for shapes a bare class can't express: list[int] (a list of ints), dict[str, int] (string keys, int values), int | None (an int, or nothing — the modern spelling of Optional[int]), and TypedDict for a dict with a fixed, named set of keys, each with its own type. None of these change what the function does — only what a reader, or a checker, can tell about it:

What happens when this runs?
def greet(name: str, times: int) -> str:
    return (name + " ") * times

print(greet("hi", "3"))

Catch a bug before it ships

mypy and pyright read your source without running it, walk every call, and check each argument against the function's hints. Point either one at the file above and it reports the mismatch before anything executes:

$ mypy app.py
app.py:5: error: Argument 2 to "greet" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)

$ pyright app.py
app.py:5:18 - error: Argument of type "Literal['3']" is not assignable to parameter "times"
    of type "int"  (reportArgumentType)
1 error, 0 warnings, 0 informations

Same bug, same line, caught at two different moments: mypy/pyright catch it before the program runs at all; without them, Python only finds out when that exact line executes — a code path a quick test run might never reach.

Validate data at the door

Hints describe shape to a checker. Pydantic goes further and actually enforces a shape at runtime, which matters most at the edges of a program — data arriving from a request body, a config file, a database row, none of which ever ran through mypy:

from pydantic import BaseModel

class Order(BaseModel):
    item: str
    quantity: int

Order(item="Pens", quantity=3)        # fine — builds the object
Order(item="Pens", quantity="three")  # raises ValidationError, right here, at runtime

"three" can't become an int, so Pydantic raises immediately — the one place in this lesson a type mismatch actually stops a program on its own. That's the trade mypy/pyright don't make: no setup, no separate checking step, but the cost is paid on every real call instead of once before shipping.

Three more names worth knowing by what they're for, not what they check: pyenv installs and switches between Python versions themselves (3.11 vs 3.12) — nothing to do with typing. Pipenv and virtualenv manage per-project environments, the same job venv does (section-15 covers that in full) — Pipenv adds a lockfile on top. pyre is Meta's own static checker, a third alternative to mypy and pyright, same idea, different tool.

Quick practice

What does this print?
def area(width: float, height: float) -> float:
    return width * height

print(area(3, 4), type(area(3, 4)))
What does this print?
def ship(weight: "int"):
    pass

print(ship.__annotations__)
quick drill

mypy reports an error on a line that Python runs without crashing every single time in production. What does that mean?

What does this print?
from typing import Optional

def find_user(id: int) -> Optional[str]:
    return None

result = find_user(5)
print(result is None, result == "")

🎯 Your turn

Write type_check(args). It checks a fixed function, safe_divide(a: int, b: int) -> float, against the two values in args (positionally: args[0] is a, args[1] is b). Use safe_divide.__annotations__ and safe_divide.__code__.co_varnames to find each parameter's expected type, and isinstance to check the matching value. Return True only if every value matches its hint.

  • type_check([10, 2]) → True
  • type_check([10, "2"]) → False

Hint: names = safe_divide.__code__.co_varnames[:safe_divide.__code__.co_argcount] gives you ("a", "b") in order; zip(names, args) pairs each one with its value.

Press ▶ Run, try the Live App chips, then ✓ Check. After that, the practice set below builds a small runtime validator out of the same annotation-reading idea.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upSpot an optional hint

Write is_optional(hint). hint is a type written as a string, like "int | None" or "Optional[int]" or "str". Return True if the hint allows None.

optional
DrillStrip to the bare type name

Write base_type(hint). Given a hint string, return just the underlying type name: "list[int]" -> "list", "int | None" -> "int", "Optional[str]" -> "str", "int" -> "int".

type-annotations
BuildCheck a value against a type name

Write matches_type(value, type_name). type_name is one of "int", "str", "float", "bool", "list", "dict". Return whether value is actually that type. An unrecognised type_name should return False.

type-annotations
BossValidate a record against a schema

Write validate_record(record, schema). schema is a dict of {field: type_name}. Return True only if every field in schema is present in record AND matches its type (reuse the same type-name lookup as matches_type).

pydantic
CapstoneValidate a record with optional fields

Write validate_optional_record(record, schema). schema values are hint strings like "str" or "int | None". A field whose hint is optional may be missing from record or None. A field whose hint is NOT optional must be present and non-None, and must match its base type.

optionalpydantic
type_check.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
type_check(args) → boolTrue if every positional value matches safe_divide's annotation for that parameter.
try:
returned
No app map for this section yet.
Press Run to execute your code.
Press Check to run the tests for this lesson.