Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 11 – Type hints and static type checking

Overloads, ParamSpec, Self and @override

Give a function return types that depend on its arguments, keep decorator signatures with ParamSpec, chain methods with Self, and confirm overrides.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: An overload gives each argument type its own return type · ParamSpec keeps a decorated function's signature · Self keeps the subclass type through a chain of calls · 3 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Describe return types that depend on the argument types with @overload
  • Type decorators that keep the wrapped function's parameters with ParamSpec and Concatenate
  • Use Self for methods and alternate constructors that return the instance
  • Use @override so that the checker confirms each method really overrides one
  • Type **kwargs key by key with Unpack and a TypedDict

Before you start

A retry decorator: A function that takes a function and returns the one its name should refer to, usually a wrapper. @timed above def report(): has the same effect as report = timed(report) after it. More in “Decorators: wrapping functions cleanly” typed with Callable[..., Any] erases the signature of every function it wraps: in this lesson’s run, mypy saw the decorated function as def (*Any, **Any) -> Any, and a call with timeout="slow" passed the check. Plain annotations cannot describe such signatures, and four tools of the typing module can.

overload: One of several signatures declared for a single function, each a stub marked @overload with ... as its body. A type checker gives a call the return type of the first stub that fits it; at run time only the implementation after the stubs runs. give one function several signatures, so a checker knows that clean("text") returns a str and clean(b"data") returns bytes. ParamSpec: A type parameter that stands for a whole parameter list, written **P in def retry[**P, R]. A decorator typed Callable[P, R] in and out hands the wrapped function's parameters on, so wrong calls are still reported (Python 3.10, PEP 612). lets a decorator keep the parameters of the function it wraps. Self type: The type Self, from typing (Python 3.11, PEP 673): the type of the object a method was called on. A method annotated -> Self returns a PagedQuery when called on a PagedQuery, so a chain of calls keeps a subclass's own methods. types a method that returns its own instance, so a chain of calls keeps a subclass’s type. The @override: A decorator from typing (Python 3.12, PEP 698) that marks a method as overriding one in a base class. A type checker reports the method when no base class has a method of that name; at run time it only sets __override__ to True. decorator asks the checker to confirm that a method really overrides one in a base class. Three more come later. Concatenate types decorators that add or remove a function’s first parameters, Unpack with a TypedDict types **kwargs key by key, and TypeVarTuple stands for any number of types.

An overload gives each argument type its own return type

clean() strips spaces from text or bytes, so a caller who passes a str should get a str back. Here it is with overloads and with a union return type:

clean() with overloads, clean_union() without Python · overloads/clean.py
"""One function, two signatures: @overload tells the checker which type comes back for which argument."""
from typing import get_overloads, overload, reveal_type


@overload
def clean(value: str) -> str: ...
@overload
def clean(value: bytes) -> bytes: ...
def clean(value: str | bytes) -> str | bytes:
    return value.strip()


def clean_union(value: str | bytes) -> str | bytes:
    return value.strip()


name = clean("  Asha Rao ")
reveal_type(name)
print(name.casefold())

other = clean_union("  Ravi ")
reveal_type(other)
print(other.casefold())

print("overloads registered:", len(get_overloads(clean)))

Output

asha rao
ravi
overloads registered: 2

Printed as an error (standard error)

Runtime type is 'str'
Runtime type is 'str'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 clean.py

mypy on clean.py Python · overloads/check_clean.py
"""Runs `mypy --strict clean.py` through mypy's Python API and prints what the command prints."""
from mypy import api

print("$ mypy --version")
print(api.run(["--version"])[0], end="")
print("$ mypy --strict clean.py")
report, errors, status = api.run(["--strict", "clean.py"])
print(report + errors, end="")

Output

$ mypy --version
mypy 2.4.0 (compiled: yes)
$ mypy --strict clean.py
clean.py:18: note: Revealed type is "str"
clean.py:22: note: Revealed type is "str | bytes"
clean.py:23: error: Item "bytes" of "str | bytes" has no attribute "casefold"  [union-attr]
Found 1 error in 1 file (checked 1 source file)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_clean.py

The check scripts run mypy 2.4.0 from Python; the line after each dollar sign is the same check as a terminal command. The two stubs marked @overload declare the signatures, with ... as their body; the definition after them, without the decorator, is the implementation that actually runs and must accept every argument the stubs allow. mypy picked the first stub that matched the call and showed str, so name.casefold() passed. clean_union() does the same work with a union return type, and mypy reported the call to casefold(), because a caller of that function can never know which of the two types it got back. Step through line 17, as mypy checks it and as Python runs it:

Interactive · step by step One call of clean(), as mypy reads it and as Python runs it Step through line 17 of clean.py. The left column is what mypy reads before the program runs; the right column is what Python runs.
One call of clean(), as mypy reads it and as Python runs it, step 6 of 6. Showing clean(" Asha Rao "), stub 1: str -> str (kept for get_overloads()), stub 2: bytes -> bytes (kept for get_overloads()), name: str, the implementation, printed: asha rao. In focus: stub 1: str -> str, stub 2: bytes -> bytes.tried firstit fitswhat runsclean(" Asha Rao ")line 17, a str argumentstub 1: str -> strkept for get_overloads()stub 2: bytes -> byteskept for get_overloads()name: strshown on line 18the implementationvalue: str | bytesprinted: asha raocasefold() of a str

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The call. Line 17 calls clean() with a str. The file declares two signatures, each a stub marked @overload whose body is ..., and then one definition without the decorator that does the work.
  2. mypy tries stub 1. mypy compares the call with the stubs in the order they are written. Stub 1 takes a str and the argument is a str, so it fits, and mypy looks no further.
  3. The call gets str. The call takes the return type of the stub that fitted. reveal_type() showed str on line 18, so name.casefold() on line 19 passes the check.
  4. Python skips the stubs. At run time no stub is consulted. The definition without @overload came last and replaced them, so every call of clean() runs the implementation, which accepts str | bytes.
  5. The same answer, twice. The implementation returned a str, and casefold() printed asha rao. The checker's answer and the run agree, and only the checker needed the stubs to know it in advance.
  6. Stubs kept for tools. The decorator records every stub it marks, which is how typing.get_overloads(clean) counted 2 of them at the end of the run.

Use overloads when the return type follows from the argument types; a union return type is right only when the result really can be either. The order of the stubs matters, because the checker uses the first one that fits, so the most specific signature goes first. At run time only the implementation exists, and typing.get_overloads() (Python 3.11) lists the stubs for tools that want them.

Checkpoint

Check: Overloads, a return type per argument type

  1. Question 1 of 2 A function has two overloads, (value: object) -> str first and (value: int) -> int second. What type does the checker give tag(3)?

    Choose one answer.

    A hint

    The checker reads the stubs in the order they are written.

    Show the answer: Question 1 of 2

    Answer: str

    Stubs are tried in order and the first that fits wins, which is why the narrowest signature goes first. With the broad stub first, tag(3) is a str for the checker even though the implementation returns the int.

  2. Question 2 of 2 When the program runs clean(" Asha Rao "), which definition of clean runs?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The implementation, the definition without @overload

    Each @overload stub is recorded and then replaced by the next definition of the same name, so after the last one the name clean is the implementation. typing.get_overloads(clean) still found the 2 stubs, for tools that want them.

ParamSpec keeps a decorated function’s signature

Here is the opening’s retry decorator, typed two ways around the same fetch(), with what each way leaves the checker to know:

A decorator typed with Callable[..., Any] drops the signature of fetch(); one typed with ParamSpec P and R keeps it.fetch(url: str, timeout: float = 5.0) -> bytes@retry_loose(3)typed with Callable[..., Any]@retry(3)typed with Callable[P, R]P = (url: str, timeout: float = ...)R = bytesfetch, as the checker sees it:def (*Any, **Any) -> Anyevery call passesfetch, as the checker sees it:def (url: str, timeout: float =) -> bytesa wrong call is reportedsignature droppedsignature captured

What the checker knows about a decorated function

Text description of the diagram

The diagram starts with one function, fetch(url: str, timeout: float = 5.0) -> bytes, and follows it through two decorators.

  • On the first path, the decorator retry_loose(3) is typed with Callable[..., Any]. The signature is dropped, and the checker sees the decorated fetch as def (*Any, **Any) -> Any, so every call passes, right or wrong.
  • On the second path, the decorator retry(3) is typed with Callable[P, R]. The ParamSpec P captures the parameters (url: str, timeout: float = ...) and R captures the return type bytes. The checker sees the decorated fetch with its original signature, so a wrong call is reported.
A retry decorator typed two ways Python · decorators/retry.py
"""Two retry decorators: one typed with Callable[..., Any], one with ParamSpec, around the same function."""
import functools
from collections.abc import Callable
from typing import Any, reveal_type


def retry_loose(times: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    def decorate(fn: Callable[..., Any]) -> Callable[..., Any]:
        @functools.wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            for attempt in range(1, times + 1):
                try:
                    return fn(*args, **kwargs)
                except ConnectionError:
                    if attempt == times:
                        raise
        return wrapper
    return decorate


def retry[**P, R](times: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
    def decorate(fn: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(fn)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            for attempt in range(1, times):
                try:
                    return fn(*args, **kwargs)
                except ConnectionError:
                    print(f"  attempt {attempt} failed, trying again")
            return fn(*args, **kwargs)
        return wrapper
    return decorate


calls = 0


@retry(3)
def fetch(url: str, timeout: float = 5.0) -> bytes:
    global calls
    calls += 1
    if calls < 3:
        raise ConnectionError("reset by peer")
    return f"{url} in under {timeout}s".encode()


@retry_loose(3)
def fetch_loose(url: str, timeout: float = 5.0) -> bytes:
    return url.encode()


reveal_type(fetch)
reveal_type(fetch_loose)
print(fetch("https://example.org"))
print(fetch_loose("https://example.org", timeout="slow"))
print(fetch("https://example.org", timeout="slow"))

Output

  attempt 1 failed, trying again
  attempt 2 failed, trying again
b'https://example.org in under 5.0s'
b'https://example.org'
b'https://example.org in under slows'

Printed as an error (standard error)

Runtime type is 'function'
Runtime type is 'function'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 retry.py

mypy on retry.py Python · decorators/check_retry.py
"""Runs `mypy --strict retry.py` through mypy's Python API and prints what the command prints."""
from mypy import api

print("$ mypy --version")
print(api.run(["--version"])[0], end="")
print("$ mypy --strict retry.py")
report, errors, status = api.run(["--strict", "retry.py"])
print(report + errors, end="")

Output

$ mypy --version
mypy 2.4.0 (compiled: yes)
$ mypy --strict retry.py
retry.py:52: note: Revealed type is "def (url: str, timeout: float =) -> bytes"
retry.py:53: note: Revealed type is "def (*Any, **Any) -> Any"
retry.py:56: error: Argument "timeout" to "fetch" has incompatible type "str"; expected "float"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_retry.py

retry_loose() is typed with Callable[..., Any], so the function it returns accepts any arguments and returns Any: mypy showed def (*Any, **Any) -> Any and said nothing about timeout="slow". retry() declares a ParamSpec P and a return type R in its type parameters, def retry[**P, R]. P captures the whole parameter list of the wrapped function and R its return type, and the wrapper’s *args: P.args, **kwargs: P.kwargs passes them through, so the decorated fetch keeps def (url: str, timeout: float =) -> bytes and the bad call on line 56 is reported. Python still ran it, and the last line of output shows the string inside the result: only the checker reads annotations.

Checkpoint

Check: ParamSpec in decorators

  1. Question 1 of 2 A decorator is typed (fn: Callable[..., Any]) -> Callable[..., Any]. What does mypy show for a function it decorates?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: def (*Any, **Any) -> Any

    The checker knows a decorated function only through the decorator's annotations. With Callable[..., Any] those say "any arguments, any result", so every call passes, right or wrong.

  2. Question 2 of 2 fetch is decorated with @retry(3), which is typed with Callable[P, R], and keeps (url: str, timeout: float = 5.0) -> bytes. Which call does mypy report?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: fetch("https://example.org", timeout="slow")

    P captures the whole parameter list, names and defaults included, and R the return type, so the decorated fetch is checked exactly like the original. Only the string timeout breaks its signature.

Self keeps the subclass type through a chain of calls

Each method of this query builder returns the instance, so calls chain, and a subclass adds page():

A query builder chained through a subclass Python · builders/query.py
"""Methods that return the instance: annotated with Self, the chain keeps a subclass's own type."""
from typing import Self, reveal_type


class Query:
    def __init__(self) -> None:
        self.parts: list[str] = []

    def where(self, condition: str) -> Self:
        self.parts.append(f"where {condition}")
        return self

    def where_named(self, condition: str) -> "Query":
        self.parts.append(f"where {condition}")
        return self


class PagedQuery(Query):
    def page(self, number: int) -> Self:
        self.parts.append(f"page {number}")
        return self


q = PagedQuery().where("city = 'Pune'").page(2)
reveal_type(q)
print(q.parts)
q2 = PagedQuery().where_named("city = 'Goa'").page(3)
print(q2.parts)

Output

["where city = 'Pune'", 'page 2']
["where city = 'Goa'", 'page 3']

Printed as an error (standard error)

Runtime type is 'PagedQuery'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 query.py

mypy on query.py Python · builders/check_query.py
"""Runs `mypy --strict query.py` through mypy's Python API and prints what the command prints."""
from mypy import api

print("$ mypy --version")
print(api.run(["--version"])[0], end="")
print("$ mypy --strict query.py")
report, errors, status = api.run(["--strict", "query.py"])
print(report + errors, end="")

Output

$ mypy --version
mypy 2.4.0 (compiled: yes)
$ mypy --strict query.py
query.py:25: note: Revealed type is "query.PagedQuery"
query.py:27: error: "Query" has no attribute "page"  [attr-defined]
Found 1 error in 1 file (checked 1 source file)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_query.py

where() is annotated -> Self (Python 3.11, PEP 673), so on a PagedQuery it returns a PagedQuery, and the chain can go on to page(). where_named() names the class instead, and mypy reported the .page(3) call after it, although the program ran both chains, because the object was a PagedQuery all along. Self describes “the type of the object this method was called on”, which is what fluent builders, copy() methods and alternate constructors return. Self is valid only inside a class, where it stands for that class or a subclass. Step through both chains:

Interactive · step by step Two chains on a PagedQuery, with Self and without Step through both chains of query.py. The sub-label of each call is the type mypy gave its result.
Two chains on a PagedQuery, with Self and without, step 6 of 6. Showing PagedQuery(), .where(...) -> Self, .page(2), PagedQuery(), .where_named(...) (the checker now sees Query), .page(3) (ran: printed page 3). In focus: .page(2), .page(3).PagedQuery()type: PagedQuery.where(...) -> Selfstill a PagedQuery.page(2)passes the checkPagedQuery()type: PagedQuery.where_named(...)the checker now sees Query.page(3)ran: printed page 3

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Two chains, one class. Both chains start with PagedQuery(), a subclass of Query that adds page(). Lines 24 and 27 differ only in which filter method they call first.
  2. where() returns Self. where() is annotated -> Self, the type of the object it was called on. Called on a PagedQuery, it gives the checker a PagedQuery back.
  3. page() can follow. The checker still knows the subclass, so .page(2) is found, and reveal_type() showed PagedQuery for the whole chain on line 25.
  4. where_named() names Query. where_named() does the same work but is annotated -> "Query". After it, the checker knows only the base class, whatever object came back.
  5. The chain breaks for mypy. Query has no page(), so mypy reported .page(3) on line 27, a line that is correct at run time.
  6. What actually ran. Both chains ran and printed their parts, because where_named() returned the same PagedQuery it was called on. Self changes what the checker knows, never what runs.

Checkpoint

Check: Self for methods that return the instance

  1. Question 1 of 2 Query.where() is annotated -> Self, and class PagedQuery(Query) adds page(). What type does the checker give PagedQuery().where("x")?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: PagedQuery

    A method annotated -> Self returns, for the checker, the class of the object it was called on. Called on a PagedQuery it returns a PagedQuery, so the chain keeps the subclass's own methods.

  2. Question 2 of 2 mypy reported .page(3) after where_named() on line 27. What happened when the program ran that line?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It ran and printed the parts, because the object was a PagedQuery

    Both chains printed their parts. Self or a class name in an annotation changes only what the checker knows; the object that comes back is the same either way.

@override turns a stale override into a checker error

The base class below renamed its method from send_message() to send(). The SMS subclass still overrides the old name:

An override left behind by a rename Python · overrides/notifier.py
"""@override after a rename: the base method is now send(), and one subclass still overrides the old name."""
from typing import override


class Notifier:
    def send(self, to: str, text: str) -> None:
        print(f"plain message to {to}: {text}")

    def deliver(self, to: str, text: str) -> None:
        self.send(to, text)


class SmsNotifier(Notifier):
    @override
    def send_message(self, to: str, text: str) -> None:
        print(f"SMS to {to}: {text[:20]}")


class EmailNotifier(Notifier):
    def send(self, to: str, text: str) -> None:
        print(f"email to {to}: {text}")


SmsNotifier().deliver("customer 17", "Your parcel is out for delivery")
EmailNotifier().deliver("customer 17", "Your invoice is ready")
print("marked:", getattr(SmsNotifier.send_message, "__override__", False), getattr(EmailNotifier.send, "__override__", False))

Output

plain message to customer 17: Your parcel is out for delivery
email to customer 17: Your invoice is ready
marked: True False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 notifier.py

mypy on notifier.py Python · overrides/check_notifier.py
"""Runs `mypy --strict notifier.py` through mypy's Python API and prints what the command prints."""
from mypy import api

print("$ mypy --version")
print(api.run(["--version"])[0], end="")
print("$ mypy --strict notifier.py")
report, errors, status = api.run(["--strict", "notifier.py"])
print(report + errors, end="")

Output

$ mypy --version
mypy 2.4.0 (compiled: yes)
$ mypy --strict notifier.py
notifier.py:15: error: Method "send_message" is marked as an override, but no base method was found with this name  [misc]
Found 1 error in 1 file (checked 1 source file)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_notifier.py

Python ran the inherited send(), so customers got a plain message instead of an SMS, and nothing failed. The @override decorator (Python 3.12, PEP 698) on send_message() made mypy look for a base method with that name and report that there is none. At run time the decorator only sets __override__ on the method, as the last line of output shows; the checking is the checker’s job. Follow the first delivery as Python runs it, then the check:

Interactive · stage by stage How a renamed base method left an override behind Step through the first delivery of notifier.py, then what mypy does with the @override line before the program runs.
How a renamed base method left an override behind, step 7 of 7. Showing SmsNotifier().deliver(...), deliver() calls self.send(), in SmsNotifier?, in Notifier?, a plain message is sent, @override on line 14, error on line 15. In focus: error on line 15.SmsNotifier().deliver(...)line 24deliver() calls self.send()Python looks the name upin SmsNotifier?only send_message() therein Notifier?yes: the plain send()a plain message is sentno error, and no SMS@override on line 14mypy reads the baseserror on line 15no such base method

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. A delivery starts. Line 24 asks an SmsNotifier to deliver a message. SmsNotifier defines no deliver(), so Python runs the one it inherits from Notifier.
  2. send() is looked up. deliver() calls self.send(). Python looks for send first on the object's own class, then on its base classes, in order.
  3. Not in the subclass. SmsNotifier has no send(). Its method is still called send_message(), the name the base class used before the rename.
  4. Found in the base. Notifier has send(), so Python calls it. Nothing in the lookup fails, because a method that overrides nothing is just one more method.
  5. The wrong message. The customer got a plain message instead of an SMS, as the first line of the run shows, and the program raised no error at all.
  6. What @override asks. Before the program runs, mypy reads @override on send_message() and looks for a method of that name in Notifier, the base class.
  7. The rename, reported. There is none, so mypy reported line 15, the very line that must change. Renaming the method to send() makes the SMS go out again.

Key idea

Mark every override with @override, and a renamed or removed base method becomes a checker error at the line that must change.

Checkpoint

Check: @override and a renamed base method

  1. Question 1 of 2 What does @override do when the program runs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It sets __override__ to True on the method and checks nothing

    @override is a promise for the checker: mypy looks for a base method with the same name and reports the line when there is none. At run time the decorator only marks the method.

  2. Question 2 of 2 A base class renames notify() to alert(). A subclass still defines notify(), marked @override. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The checker reports notify(), because no base class has it any more

    Without the decorator the rename would pass silently, and alert() would run the base class's version for every subclass. With it, the stale name is a checker error at the line that must change.

Which tool fixes which signature problem

Find the problem your signature has in the middle column:

Tool, and since when The problem it solves At run time
Overloads, Python 3.5 A return type that depends on the argument types Only the implementation runs
ParamSpec, Python 3.10 A decorator that would lose the wrapped function’s signature Nothing changes
Self, Python 3.11 A method that returns the instance, in subclasses too Nothing changes
@override, Python 3.12 An override whose base method was renamed or removed It sets __override__ on the method

Version note

The versions are those of the standard typing module; projects that support older Pythons import the same names from the typing_extensions package.

Checkpoint

Check: Choosing the tool for a signature

  1. Question 1 of 2 A @timed decorator logs how long each call takes. Since it was added, editors show (*args, **kwargs) for every decorated function, and wrong calls pass. What fixes it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Type it with ParamSpec, Callable[P, R] in and out

    A decorator that hides its function's parameters is the problem ParamSpec solves: P captures them and gives them back to the decorated function.

  2. Question 2 of 2 parse(data) returns a dict when data is a str and a list when it is bytes. Today it is annotated -> dict | list, and every caller checks the result. Which tool fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Overloads, one stub per argument type

    When each argument type decides the result type, write one overload per case. The checker then gives parse("…") a dict and parse(b"…") a list, with no check at the call site.

Check yourself

Answer these questions one by one; the session begins with the tool you seem to know least.

Practice that adapts to you

Practise this lesson

11 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 A function has two @overload stubs and one implementation without the decorator. What runs when the program calls it?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: The implementation; the stubs are only recorded for checkers and for typing.get_overloads()

    At run time the last definition, the implementation, replaces the stubs as the function. @overload only registers each stub, which is how get_overloads(clean) found two of them in the recorded run.

  2. Question 2 of 11 clean_union() returns str | bytes. Why did mypy report other.casefold() although the call passed a str?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: The return type does not depend on the argument, so the checker must assume it may be bytes

    With a plain union return, every caller gets str | bytes, and bytes has no casefold(). The overloads of clean() tell the checker that a str argument gives a str, so name.casefold() passed.

  3. Question 3 of 11 Someone adds a third stub, (value: str | bytes) -> str | bytes, above the other two stubs of clean(). What type does a caller of clean("x") get now?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: str | bytes

    Order matters because the first stub that fits wins. A broad stub above the narrow ones hides them, so the most specific signature goes first and a catch-all, if there is one, last.

  4. Question 4 of 11 A decorator is annotated (fn: Callable[..., Any]) -> Callable[..., Any]. What does the checker know about the parameters of a function decorated with it?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Nothing: any arguments pass and the result is Any

    Callable[..., Any] means "callable with any arguments, returning anything", so mypy showed the decorated fetch_loose as def (*Any, **Any) -> Any. A ParamSpec captures the parameters instead and gives them back.

  5. Question 5 of 11 A @cached decorator should keep the full signature of any function it wraps. Which annotation does it need?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: def cached[**P, R](fn: Callable[P, R]) -> Callable[P, R]

    A decorator that keeps a signature needs both halves: P for the parameters and R for the return type, in the parameter type and in the return type alike.

  6. Question 6 of 11 What does query.py print?

    What does this program print? Choose one answer.

    """Methods that return the instance: annotated with Self, the chain keeps a subclass's own type."""
    from typing import Self, reveal_type
    
    
    class Query:
        def __init__(self) -> None:
            self.parts: list[str] = []
    
        def where(self, condition: str) -> Self:
            self.parts.append(f"where {condition}")
            return self
    
        def where_named(self, condition: str) -> "Query":
            self.parts.append(f"where {condition}")
            return self
    
    
    class PagedQuery(Query):
        def page(self, number: int) -> Self:
            self.parts.append(f"page {number}")
            return self
    
    
    q = PagedQuery().where("city = 'Pune'").page(2)
    reveal_type(q)
    print(q.parts)
    q2 = PagedQuery().where_named("city = 'Goa'").page(3)
    print(q2.parts)
    Show the answer: Question 6 of 11

    Answer: it prints

    ["where city = 'Pune'", 'page 2']
    ["where city = 'Goa'", 'page 3']

    Both chains run, because both methods return the same PagedQuery object. Only the checker sees a difference: where_named() is annotated to return Query, so mypy reported the .page(3) call after it, while where() returns Self and keeps the subclass type.

  7. Question 7 of 11 Shape.scaled(self, k: float) -> "Shape" returns a scaled copy, and Circle(Shape) adds radius. Circle(2).scaled(3).radius runs fine. What does mypy say, and what is the fix?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: It reports .radius, because scaled() returns Shape; annotate it -> Self

    A method that returns a copy of its object is exactly what Self describes. -> "Shape" makes every subclass's result a plain Shape for the checker, so subclass attributes after the call are reported.

  8. Question 8 of 11 What does the @override decorator do?

    Choose every answer that is right.

    Show the answer: Question 8 of 11

    Answer:

    • It sets the attribute __override__ to True on the method at run time
    • It lets the checker report a method that overrides nothing in a base class

    @override is for checkers: mypy reported send_message, which no base class has. At run time it only marks the method with __override__ = True, as the recorded run printed; Python checks nothing. Stopping further overrides is the job of @final.

  9. Question 9 of 11 Which of these mistakes does @override let the checker report?

    Choose every answer that is right.

    Show the answer: Question 9 of 11

    Answer:

    • A subclass method whose name is misspelt, such as sned() for send()
    • A base method that was deleted while subclasses still override it

    @override reports one kind of mistake: a method that is meant to override but overrides nothing, whether the name is misspelt, renamed in the base or deleted from it.

  10. Question 10 of 11 Which of the four tools needs Python 3.12 or newer when it is imported from the standard typing module?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: @override

    The four arrived in typing between Python 3.5 and 3.12: overload in 3.5, ParamSpec in 3.10, Self in 3.11 and override in 3.12.

  11. Question 11 of 11 A @log_calls decorator must keep signatures, and the project still runs on Python 3.9. Where does it get ParamSpec?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: from typing_extensions import ParamSpec

    The versions in the table are those of the standard typing module. Projects that support older versions import the same names from the typing_extensions package.

Decorators that keep signatures: Concatenate, async wrappers and a typed toolkit Pro: opens with a Pro, Premium or Ultimate pass

A decorator can add a parameter, remove one, or turn a function into a coroutine, and each change can be typed. This part types the common shapes, reproduces the call that only the checker catches, and ends with an exercise that uses all four tools of the lesson.

In this part:

  • Supplying the first argument with Concatenate, and an async wrapper
  • Decorator factories, the older spelling, and what Concatenate cannot do
  • Five decorator shapes, typed, and two tests that keep them so
  • Exercise: a typed toolkit
  • An overload, a timing decorator and a fluent report, typed
    • Results of the sample tests

1 diagram · 2 runnable examples (Python) · 1 exercise with sample tests · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Overloads, Self and @override in real code: the rules checkers enforce, reproduced Pro: opens with a Pro, Premium or Ultimate pass

Overloads have rules about order and overlap, Self has an older spelling that libraries still use, and @override can be made mandatory. This part reproduces the errors behind each rule, then types **kwargs key by key.

In this part:

  • Overload rules: a Literal flag, the fallback overload and two errors
  • Self or a TypeVar, in an alternate constructor
  • Making @override mandatory
  • Typing **kwargs with Unpack, and shapes with TypeVarTuple

1 diagram · 7 runnable examples (Python) · about 9 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Mid · Warm-up

    What does the @overload decorator do when the program runs?

    Almost nothing. Each function marked @overload is a stub whose body is ..., and the definition after the stubs, without the decorator, replaces them as the real function, so every call runs the implementation whatever the argument types. The decorator only records each stub, which typing.get_overloads() can list from Python 3.11, and it replaces the stub with a function that raises NotImplementedError if it is ever called directly. The overloads exist for type checkers: they choose the first stub that fits a call and give the caller its exact return type. Since Python never reads the stubs, a stub that disagrees with the implementation is found only by a checker.

  2. Mid to senior

    Type a retry decorator so that callers of the decorated function keep autocompletion and get errors for wrong arguments. What goes wrong if it is typed with Callable[..., Any], and how do you test that the typing works?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A function returns str for a str argument and bytes for a bytes argument, and today it is annotated -> str | bytes. When would you replace that with overloads, what does each choice cost the callers, and what mistakes do overloads invite?

    Model answer, Pro. First step and follow-ups

  4. Mid to staff

    After a base class renamed send_message() to send(), one subclass's override silently stopped being called and customers got the wrong notification. How does @override prevent this, and how would you make it mandatory across a large codebase?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Overloads give each argument type its own return type; the first stub that fits wins, and only the implementation runs.
  • A decorator typed Callable[P, R] in and out keeps the wrapped function’s parameters, and Callable[..., Any] checks nothing.
  • -> Self returns the type of the object a method was called on, so a chain on a subclass keeps its methods.
  • @override makes the checker report a method that overrides nothing; at run time it only sets __override__.

Terms in this lesson

@override
A decorator from typing (Python 3.12, PEP 698) that marks a method as overriding one in a base class. A type checker reports the method when no base class has a method of that name; at run time it only sets __override__ to True.
overload
One of several signatures declared for a single function, each a stub marked @overload with ... as its body. A type checker gives a call the return type of the first stub that fits it; at run time only the implementation after the stubs runs.
ParamSpec
A type parameter that stands for a whole parameter list, written **P in def retry[**P, R]. A decorator typed Callable[P, R] in and out hands the wrapped function's parameters on, so wrong calls are still reported (Python 3.10, PEP 612).
Self type
The type Self, from typing (Python 3.11, PEP 673): the type of the object a method was called on. A method annotated -> Self returns a PagedQuery when called on a PagedQuery, so a chain of calls keeps a subclass's own methods.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.