Nástroje pro statické typové kontroly v ekosystému jazyka Python

5. 5. 2026
Doba čtení: 41 minut

Sdílet

Autor: Root.cz s využitím DALL-E
Dnes se budeme zabývat třemi nástroji, které v ekosystému jazyka Python dokážou provádět statické typové kontroly: Mypy, Pyright a Ty. Liší se od sebe jak kvalitou výsledků, tak i přehledností či rychlostí.

Statické typové kontroly

Co se dozvíte v článku
  1. Statické typové kontroly
  2. Od čistě dynamicky typovaných jazyků k jazykům s volitelnými statickými typy
  3. Roztříštěný svět nástrojů pro kontrolu datových typů v Pythonu
  4. Nástroje pro provádění statických typových kontrol Pythonu
  5. Konfigurace nástrojů pro provádění statických typových kontrol
  6. Vytvoření projektu a instalace všech potřebných balíčků
  7. Detekce chybějících informací o typech parametrů funkce i typu její návratové hodnoty
  8. Kontrola volání funkce s plnými typovými informacemi
  9. Kontrola volání metod s předáváním parametrů
  10. Kontrola korektního použití dekorátoru @override
  11. Detekce rozdílu mezi běžným slovníkem a typem Mapping
  12. Opačné chování: modifikace slovníku typu frozendict
  13. Modifikace množiny typu frozenset
  14. Přiřazení nové hodnoty do konstanty, modifikace konstanty
  15. Detekce pokusu o modifikaci neměnitelné n-tice
  16. Problém variance v typovém systému Pythonu
  17. Rychlost provádění statických typových kontrol
  18. Repositář s demonstračními příklady
  19. Odkazy na další články o typovém systému Pythonu
  20. Odkazy na Internetu

„If it walks like a duck, and talks like a duck, then it is a duck“
(základní myšlenka, na které byl založen nejenom typový systém programovacího jazyka Python)

Programovací jazyk Python od verze 3.5 podporuje zápis takzvaných typových anotací resp. typových nápověd (type annotations, type hints). Jedná se o nepovinnou (zcela dobrovolnou) specifikaci typů parametrů funkcí a metod, návratových hodnot funkcí a metod, typů proměnných atd. A právě zápis typových anotací do značné míry umožňuje provedení statických typových kontrol.

Myšlenka, na níž stojí statická typová kontrola, je snadno pochopitelná, protože se do značné míry podobá dalším analýzám kódu (které provádí překladač, interpret, lintery atd.). Celá myšlenka je založena na tom, že u každé proměnné deklarované v programu, u každého parametru funkce a taktéž u každého návratového parametru (divný termín, že?) funkce se přímo či nepřímo uvede datový typ (nepřímo v případě, že jazyk umí typ odvodit z použité hodnoty – jedná se o takzvanou typovou inferenci). Díky tomu, že je specifikace typu proměnné/parametru/návratové hodnoty dostupná přímo ve formě zdrojového kódu, může být typová kontrola skutečně statická – nevyžaduje tedy, aby se program spustil. To má své nesporné výhody, protože takto specifikované informace o typech dokáží zpracovat i moderní (a nejenom moderní) integrovaná vývojová prostředí, která ji mohou použít v kontextové nápovědě atd.

Ovšem současně zde narážíme na značnou nevýhodu: je totiž velmi složité vytvořit snadno použitelný a současně i staticky typovaný programovací jazyk. A další nevýhodou je, že zápis datových typů je vyžadován i v případě, že se tvoří jednoduché skripty nebo prototypy. Proto není divu, že mnoho programovacích jazyků (a nutno říci, že mnohdy velmi úspěšných jazyků) striktní zápis datových typů nevyžaduje a tím pádem nebude (zcela) dostupná statická typová kontrola.

Poznámka: u staticky typovaných programovacích jazyků provádí základní typové kontroly už samotný překladač resp. interpret. Ovšem jak uvidíme v dalším textu, v závislosti na použitém typovém systému (různé typy variance atd.) se může stát, že některé typové kontroly musí být přesunuty z času překladu (compile time) do času běhu aplikace (runtime).

Od čistě dynamicky typovaných jazyků k jazykům s volitelnými statickými typy

Pokud se podíváme na seznam v současnosti nejpopulárnějších a nejpoužívanějších programovacích jazyků, nalezneme zde jak typické zástupce dynamicky typovaných jazyků, tak i zástupce jazyků se statickými typovými systémy:

Dynamicky typovaný Staticky typovaný
Python C
JavaScript C++
Ruby Go
Perl Rust
Matlab Java
PHP Scala
Poznámka: druhou, nezávislou osou, by bylo rozdělení podle toho, zda je typový systém silný, či slabý.

Zaměřme se nyní na první tři zmíněné dynamicky typované programovací jazyky, tedy konkrétně na Python, JavaScript a Ruby. Tyto jazyky se původně používaly na tvorbu relativně krátkých skriptů (v případě JavaScriptu typicky běžících v rámci stránky prohlížeče), ovšem postupně se rozšířily i do mnoha dalších oblastí, takže v nich začaly vznikat i velmi rozsáhlé aplikace. A právě u rozsáhlejších aplikací se začal ukazovat význam staticky zapisovaných a taktéž staticky kontrolovaných datových typů. Proto pro tyto programovací jazyky postupně vznikla rozšíření, která do nich přidává volitelný zápis datových typů. A tato rozšíření umožňují statickou kontrolu datových typů s využitím k tomu určených nástrojů:

Původní jazyk Rozšíření o statické datové typy
JavaScript TypeScript, Flow
Python mypy, Pyright, Pyrety
Ruby Sorbet

Již na začátku je nutné připomenout, že Python stále zůstává dynamicky typovaným jazykem a typové anotace jsou sice součástí specifikace jazyka (PEP 484 – Type Hints), ale nejsou striktně vyžadovány. U krátkých skriptů podle mého názoru většinou i postrádají smysl, ovšem pro rozsáhlejší projekty bývají nepostradatelným pomocníkem (a to ještě ve větší míře v době AI slopu).

Roztříštěný svět nástrojů pro kontrolu datových typů v Pythonu

Tuto kapitolu si dovolím začít poněkud neobvyklým způsobem, a to konkrétně citací ze slavného Zen of Python. Tato sada aforismů je přímo součástí samotného Pythonu, o čemž je snadné se přesvědčit:

$ python
Python 3.13.9 (main, Oct 14 2025, 00:00:00) [GCC 15.2.1 20250808 (Red Hat 15.2.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.

Do interpretru Pythonu nyní postačuje zadat příkaz pro import speciálního modulu this:

>>> import this
The Zen of Python, by Tim Peters
 
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Důležitá je především věta:

There should be one-- and preferably only one --obvious way to do it.

Bohužel se tento aforismus v praxi týká jen samotného programovacího jazyka Python a nikoli celého ekosystému Pythonu. Ten je totiž naopak velmi roztříštěný – ostatně se stačí podívat na to, kolik existuje správců balíčků, kolik správců projektů – a také nástrojů pro statickou typovou kontrolu. Dnes se budeme zabývat právě poslední skupinou nástrojů, tj. konkrétně lintery, které dokážou detekovat korektní i nekorektní použití datových typů ve zdrojových kódech.

Nástroje pro provádění statických typových kontrol Pythonu

Jak jsme si již naznačili v předchozí kapitole, vzniklo pro ekosystém programovacího jazyka Python hned několik nástrojů určených pro provádění statických typových kontrol. Tyto nástroje se od sebe liší jak úspěšností nalezení problémů ve zdrojových kódech, tak i svou rychlostí. Podívejme se nejdříve na úspěšnost nalezení problémů ve zdrojových kódech; hodnoty byly změřeny pro sadu „typových“ testů. Tato úspěšnost prozatím není pro žádný nástroj stoprocentní (což ovšem může naznačovat zbytečnou složitost typového systému Pythonu), ovšem z následující tabulky je patrný velký rozdíl oproti přesnosti (například) nástroje pyright na straně jedné a ty na straně druhé (údaje u Mypy jsou poněkud zavádějící, protože chování Mypy do značné míry závisí na konfiguraci):

Nástroj Ukončené testy Poměr v procentech
pyright 136/139 97,8%
Zuban 134/139 96,4%
Pyrefly 122/139 87,8%
mypy 81/139 58,3%
ty 74/139 53,2%

Rychlost jednotlivých nástrojů si ještě změříme na rozsáhlejším (reálném) projektu.

Konfigurace nástrojů pro provádění statických typových kontrol

Chování všech nástrojů pro provádění statických typových kontrol je možné ovlivnit buď při spouštění z příkazového řádku nebo uložením konfiguračních voleb do vhodného konfiguračního souboru. O jaký soubor se jedná je vypsáno v následující tabulce. V současnosti se v ekosystému Pythonu rozšířilo použití „univerzálního“ projektového souboru pyproject.toml, do kterého je možné ukládat různé konfigurační volby. Typicky se pro každý nástroj používá samostatná sekce. A opět platí, že většina nástrojů určených pro statickou typovou kontrolu podporuje uložení konfigurace do pyproject.toml, a to do sekce vypsané níže:

Nástroj Konfigurační soubor Sekce v projektovém souboru
mypy mypy.ini, .mypy.ini, setup.cfg, pyproject.toml [tool.mypy]
pyright pyrightconfig.json, pyproject.toml [tool.pyright]
Basedpyright pyrightconfig.json, basedpyrightconfig.json, pyproject.toml [tool.basedpyright] a také [tool.pyright]
ty ty.toml, pyproject.toml [tool.ty]
Pyrefly pyrefly.toml není podporováno
Zuban mypy.ini, setup.cfg, pyproject.toml (režim mypy); pyproject.toml [tool.mypy] nebo [tool.zuban]
Poznámka: nástroj mypy nabízí mnoho konfiguračních voleb i přepínačů, které je možné zadat na příkladovém řádku. Většina podrobnějších kontrol se zapne přepínačem –strict, kterým se povolí tyto volby:
disallow_untyped_defs
disallow_incomplete_defs
disallow_untyped_calls
disallow_untyped_decorators
disallow_any_generics
disallow_subclassing_any
check_untyped_defs
warn_return_any
warn_unused_configs
warn_redundant_casts
warn_unused_ignores
strict_equality
strict_bytes
no_implicit_reexport
extra_checks

Vytvoření projektu a instalace všech potřebných balíčků

Tento článek je zaměřen především na praktické ukázky použití nástrojů určených pro provádění statických typových kontrol. Musíme si tedy nejprve připravit projekt v Pythonu a následně do něj všechny potřebné (testované) nástroje přidat formou přímé závislosti (dependency). Pro vytvoření projektu použijeme buď nástroj PDM – viz též PDM: moderní správce balíčků a virtuálních prostředí Pythonu nebo (což je v současnosti výhodnější a taktéž rychlejší) nástroj uv:

$ uv init

nebo:

$ uv init type-checkers
 
Initialized project `type-checkers` at `/tmp/ramdisk/type-checkers/type-checkers`

Projekt bude vytvořen v aktuálním (ideálně původně prázdném) nebo nově vytvořeném adresáři a jeho projektový soubor pyproject.toml může vypadat následovně (lišit se bude verze Pythonu atd., to jsou však malé rozdíly):

[project]
name = "type-checkers"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
Poznámka: povšimněte si, že seznam závislostí je prozatím prázdný.

Nejprve do projektu přidáme jako závislost balíček ty:

$ uv add ty
 
Using CPython 3.13.9 interpreter at: /usr/bin/python3.13
Creating virtual environment at: .venv
Resolved 2 packages in 488ms
Prepared 1 package in 4.01s
Installed 1 package in 35ms
 + ty==0.0.32

Kontrola závislostí resp. přesněji řečeno výpis celého stromu závislostí:

$ uv tree
 
Resolved 2 packages in 2ms
type-checkers v0.1.0
└── ty v0.0.32

Dále přidáme balíček pyright:

$ uv add pyright
 
Resolved 5 packages in 143ms
Installed 3 packages in 701ms
 + nodeenv==1.10.0
 + pyright==1.1.408
 + typing-extensions==4.15.0

Strom závislostí nyní bude obsahovat i takzvané tranzitivní závislosti:

$ uv tree
 
type-checkers v0.1.0
├── pyright v1.1.408
│   ├── nodeenv v1.10.0
│   └── typing-extensions v4.15.0
└── ty v0.0.32

A nakonec do projektu přidáme i balíček mypy:

$ uv add mypy
 
Resolved 9 packages in 459ms
Prepared 2 packages in 4.51s
Installed 4 packages in 163ms
 + librt==0.9.0
 + mypy==1.20.2
 + mypy-extensions==1.1.0
 + pathspec==1.1.0

Výpis přímých i tranzitivních závislostí:

$ uv tree
 
type-checkers v0.1.0
├── mypy v1.20.2
│   ├── librt v0.9.0
│   ├── mypy-extensions v1.1.0
│   ├── pathspec v1.1.0
│   └── typing-extensions v4.15.0
├── pyright v1.1.408
│   ├── nodeenv v1.10.0
│   └── typing-extensions v4.15.0
└── ty v0.0.32

Nová podoba projektového souboru po přidání všech balíčků:

$ cat pyproject.toml
 
[project]
name = "type-checkers"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "mypy>=1.20.2",
    "pyright>=1.1.408",
    "ty>=0.0.32",
]

Na závěr si ještě otestujeme, jestli je skutečně možné všechny právě nainstalované nástroje spustit:

$ uv run mypy --version
 
mypy 1.17.1 (compiled: no)
$ uv run mypy --help
 
usage: mypy [-h] [-v] [-V] [more options; see below]
            [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]
 
Mypy is a program that will type check your Python code.
 
Pass in any files or folders you want to type check. Mypy will
recursively traverse any provided folders to find .py files:
 
    $ mypy my_program.py my_src_folder
 
For more information on getting started, see:
 
- https://mypy.readthedocs.io/en/stable/getting_started.html

Dále:

$ uv run pyright --version
 
pyright 1.1.408
$ uv run pyright --help
 
Usage: pyright [options] files...
  Options:
  --createstub <IMPORT>              Create type stub file(s) for import
  --dependencies                     Emit import dependency information
  -h,--help                          Show this help message
  --ignoreexternal                   Ignore external imports for --verifytypes
  --level <LEVEL>                    Minimum diagnostic level (error or warning)
  --outputjson                       Output results in JSON format
  -p,--project <FILE OR DIRECTORY>   Use the configuration file at this location
  --pythonplatform <PLATFORM>        Analyze for a specific platform (Darwin, Linux, Windows)
  --pythonpath <FILE>                Path to the Python interpreter
  --pythonversion <VERSION>          Analyze for a specific version (3.3, 3.4, etc.)
  --skipunannotated                  Skip analysis of functions with no type annotations
  --stats                            Print detailed performance stats
  -t,--typeshedpath <DIRECTORY>      Use typeshed type stubs at this location
  --threads <optional COUNT>         Use separate threads to parallelize type checking
  -v,--venvpath <DIRECTORY>          Directory that contains virtual environments
  --verbose                          Emit verbose diagnostics
  --verifytypes <PACKAGE>            Verify type completeness of a py.typed package
  --version                          Print Pyright version and exit
  --warnings                         Use exit code of 1 if warnings are reported
  -w,--watch                         Continue to run and watch for changes
  -                                  Read files from stdin

A nakonec:

$ uv run ty --version
 
ty 0.0.32
$ uv run ty --help
 
An extremely fast Python type checker.
 
Usage: ty <COMMAND>
 
Commands:
  check    Check a project for type errors
  server   Start the language server
  version  Display ty's version
  explain  Explain rules and other parts of ty
  help     Print this message or the help of the given subcommand(s)
 
Options:
  -h, --help     Print help
  -V, --version  Print version

Do projektového souboru můžeme nakonec přidat i konfigurace jednotlivých nástrojů. Příkladem je přidání konfigurace pro mypy:

[project]
name = "type-checkers"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "mypy>=1.20.2",
    "pyright>=1.1.408",
    "ty>=0.0.32",
]
 
[tool.mypy]
explicit_package_bases = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
ignore_missing_imports = true
disable_error_code = "attr-defined"
disallow_untyped_decorators = true
disallow_any_generics = true
disallow_subclassing_any = true
check_untyped_defs = true
warn_return_any = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
strict_equality = true
strict_bytes = true
no_implicit_reexport = true
extra_checks = true

Detekce chybějících informací o typech parametrů funkce i typu její návratové hodnoty

Zbylá část dnešního článku je věnována porovnání chování nástrojů pyright, mypy a ty. První příklad, který si dnes ukážeme, je triviální – necháme si zkontrolovat zdrojový kód s jedinou funkcí bez typových informací:

def add(a, b):
    return a+b

Nástroj mypy korektně nalezne, že funkce add nemá typové informace:

$ uv run mypy adder1.py
 
adder1.py:16: error: Function is missing a type annotation  [no-untyped-def]
Found 1 error in 1 file (checked 1 source file)

Nástroj pyright je v tomto ohledu shovívavější (což podle mého názoru není dobře):

$ uv run pyright adder1.py
 
0 errors, 0 warnings, 0 informations

Ani nástroj ty nenalezne žádné zásadní problémy:

$ uv run ty check adder1.py
 
All checks passed!
Poznámka: mimochodem si povšimněte, že se ty musí volat s předáním příkazu check, ostatně podobně jako například nástroj ruff pocházející od stejných autorů.

Kontrola volání funkce s plnými typovými informacemi

V dalším demonstračním příkladu si necháme zkontrolovat zdrojový kód, který obsahuje funkci s plnými typovými informacemi. Tato funkce se bude několikrát volat, přičemž se jí budou předávat parametry korektních i nekorektních typů. Budeme přitom zjišťovat, do jaké míry dokážou nástroje odhalit problematické parametry:

def add(a:int, b:int) -> int:
    return a+b
 
 
print(add(1, 2))
print(add(1, True))
print(add(1, False))
print(add(1.2, 3.4))
print(add("foo", "bar"))
print(add((1, 2), (3, 4)))
print(add([1, 2], [3, 4]))

Nástroj mypy korektně všechny problémy odhalí:

$ uv run mypy adder2.py
 
adder2.py:23: error: Argument 1 to "add" has incompatible type "float"; expected "int"  [arg-type]
adder2.py:23: error: Argument 2 to "add" has incompatible type "float"; expected "int"  [arg-type]
adder2.py:24: error: Argument 1 to "add" has incompatible type "str"; expected "int"  [arg-type]
adder2.py:24: error: Argument 2 to "add" has incompatible type "str"; expected "int"  [arg-type]
adder2.py:25: error: Argument 1 to "add" has incompatible type "tuple[int, int]"; expected "int"  [arg-type]
adder2.py:25: error: Argument 2 to "add" has incompatible type "tuple[int, int]"; expected "int"  [arg-type]
adder2.py:26: error: Argument 1 to "add" has incompatible type "list[int]"; expected "int"  [arg-type]
adder2.py:26: error: Argument 2 to "add" has incompatible type "list[int]"; expected "int"  [arg-type]
Found 8 errors in 1 file (checked 1 source file)
Poznámka: hodnoty True a False v Pythonu odpovídají hodnotám 1 a 0, což sice není hezké, ale typová kontrola musí dodržovat specifikaci jazyka.

Nástroj pyright nalezne stejné chyby, ovšem vypíše je v odlišném formátu:

$ uv run pyright adder2.py
 
/tmp/ramdisk/type-checkers/type-checkers/adder2.py
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:23:11 - error: Argument of type "float" cannot be assigned to parameter "a" of type "int" in function "add"
    "float" is not assignable to "int" (reportArgumentType)
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:23:16 - error: Argument of type "float" cannot be assigned to parameter "b" of type "int" in function "add"
    "float" is not assignable to "int" (reportArgumentType)
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:24:11 - error: Argument of type "Literal['foo']" cannot be assigned to parameter "a" of type "int" in function "add"
    "Literal['foo']" is not assignable to "int" (reportArgumentType)
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:24:18 - error: Argument of type "Literal['bar']" cannot be assigned to parameter "b" of type "int" in function "add"
    "Literal['bar']" is not assignable to "int" (reportArgumentType)
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:25:11 - error: Argument of type "tuple[Literal[1], Literal[2]]" cannot be assigned to parameter "a" of type "int" in function "add"
    "tuple[Literal[1], Literal[2]]" is not assignable to "int" (reportArgumentType)
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:25:19 - error: Argument of type "tuple[Literal[3], Literal[4]]" cannot be assigned to parameter "b" of type "int" in function "add"
    "tuple[Literal[3], Literal[4]]" is not assignable to "int" (reportArgumentType)
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:26:11 - error: Argument of type "list[int]" cannot be assigned to parameter "a" of type "int" in function "add"
    "list[int]" is not assignable to "int" (reportArgumentType)
  /tmp/ramdisk/type-checkers/type-checkers/adder2.py:26:19 - error: Argument of type "list[int]" cannot be assigned to parameter "b" of type "int" in function "add"
    "list[int]" is not assignable to "int" (reportArgumentType)
8 errors, 0 warnings, 0 informations

I nástroj ty dokáže nalézt stejné chyby:

$ uv run ty check adder2.py
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:23:11
   |
23 | print(add(1.2, 3.4))
   |           ^^^ Expected `int`, found `float`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^ ----- Parameter declared here
   |
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:23:16
   |
23 | print(add(1.2, 3.4))
   |                ^^^ Expected `int`, found `float`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^        ----- Parameter declared here
   |
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:24:11
   |
24 | print(add("foo", "bar"))
   |           ^^^^^ Expected `int`, found `Literal["foo"]`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^ ----- Parameter declared here
   |
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:24:18
   |
24 | print(add("foo", "bar"))
   |                  ^^^^^ Expected `int`, found `Literal["bar"]`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^        ----- Parameter declared here
   |
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:25:11
   |
25 | print(add((1, 2), (3, 4)))
   |           ^^^^^^ Expected `int`, found `tuple[Literal[1], Literal[2]]`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^ ----- Parameter declared here
   |
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:25:19
   |
25 | print(add((1, 2), (3, 4)))
   |                   ^^^^^^ Expected `int`, found `tuple[Literal[3], Literal[4]]`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^        ----- Parameter declared here
   |
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:26:11
   |
26 | print(add([1, 2], [3, 4]))
   |           ^^^^^^ Expected `int`, found `list[int]`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^ ----- Parameter declared here
   |
 
error[invalid-argument-type]: Argument to function `add` is incorrect
  --> adder2.py:26:19
   |
26 | print(add([1, 2], [3, 4]))
   |                   ^^^^^^ Expected `int`, found `list[int]`
   |
info: Function defined here
  --> adder2.py:16:5
   |
16 | def add(a:int, b:int) -> int:
   |     ^^^        ----- Parameter declared here
   |
 
Found 8 diagnostics
Poznámka: takto strukturovaná chybová hlášení jsou zdaleka nejpřehlednější a opět se podobají hlášením, která jsou použita v nástroji ruff.

Kontrola volání metod s předáváním parametrů

V dalším demonstračním příkladu si ověříme, jakým způsobem nástroje pro statickou typovou kontrolu odhalí předávání nekorektních hodnot do metod „typovaných“ datových kontejnerů. Poměrně dobrým příkladem je například fronta (queue), při jejíž definici je možné typovou anotací zvolit typ prvků:

import queue
 
q : queue.Queue[int] = queue.Queue()
 
q.put(42)
q.put("foo")

Do fronty s prvky typu int se snažíme přidat prvek typu string.

Nástroj Mypy tuto chybu odhalí:

$ uv run mypy --strict queue_.py
 
queue_.py:6: error: Argument 1 to "put" of "Queue" has incompatible type "str"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Totéž platí i pro nástroj Pyright, i když výsledek nemusí být příliš čitelný:

$ uv run pyright queue_.py
 
/tmp/ramdisk/type-checkers/type-checkers/queue_.py
  /tmp/ramdisk/type-checkers/type-checkers/queue_.py:6:7 - error: Argument of type "Literal['foo']" cannot be assigned to parameter "item" of type "int" in function "put"
    "Literal['foo']" is not assignable to "int" (reportArgumentType)
1 error, 0 warnings, 0 informations

Nejvíce čitelný je výstup nástroj ty:

$ uv run ty check queue_.py
 
error[invalid-argument-type]: Argument to bound method `Queue.put` is incorrect
 --> queue_.py:6:7
  |
6 | q.put("foo")
  |       ^^^^^ Expected `int`, found `Literal["foo"]`
  |
info: Method defined here
   --> stdlib/queue.pyi:100:9
    |
100 |     def put(self, item: _T, block: bool = True, timeout: float | None = None) -> None:
    |         ^^^       -------- Parameter declared here
    |
 
Found 1 diagnostic

Vyzkoušejme si ještě prakticky tentýž příklad, ovšem s obousměrnou frontou (což je mimochodem jeden z nejuniverzálnějších kontejnerů):

from collections import deque
 
q : deque[int] = deque()
 
q.append(42)
q.append("foo")

Již bez podrobnějších popisů si ukážeme zprávy vypsané všemi třemi testovanými nástroji:

$ uv run mypy --strict deque.py
 
deque.py:6: error: Argument 1 to "append" of "deque" has incompatible type "str"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)
$ uv run pyright deque.py
 
/tmp/ramdisk/type-checkers/type-checkers/deque.py
  /tmp/ramdisk/type-checkers/type-checkers/deque.py:6:10 - error: Argument of type "Literal['foo']" cannot be assigned to parameter "x" of type "int" in function "append"
    "Literal['foo']" is not assignable to "int" (reportArgumentType)
1 error, 0 warnings, 0 informations
$ uv run ty check deque.py
 
error[invalid-argument-type]: Argument to bound method `deque.append` is incorrect
 --> deque.py:6:10
  |
6 | q.append("foo")
  |          ^^^^^ Expected `int`, found `Literal["foo"]`
  |
info: Method defined here
   --> stdlib/collections/__init__.pyi:286:9
    |
286 |     def append(self, x: _T, /) -> None:
    |         ^^^^^^       ----- Parameter declared here
    |
 
Found 1 diagnostic

Kontrola korektního použití dekorátoru @override

V Pythonu se taktéž objevuje nový dekorátor, který se jmenuje override. Tento dekorátor nalezneme v balíčku typing. Podobný zápis najdeme například v Javě (kde se však jedná o anotaci – což je méně mocný nástroj než dekorátor), kde je určen ke statické kontrole při překladu, zda metoda takto označená skutečně překrývá metodu předka. Pokud například programátor napíše špatné jméno metody (překlep), nemá překladač jinou možnost jak zjistit, že se má jednat o novou metodu nebo jde o špatně zapsané jméno metody předka. V Pythonu je význam stejný, ovšem s tím, že kontrolu neprovádí překladač (CPython je ostatně interpretovaný), ale předpokládá se použití specializovaných nástrojů pro statické typové kontroly.

Použití dekorátoru @override je triviální. Postačuje ho nejprve naimportovat a posléze jím označit ty metody potomka, které překrývají metody předka. Ukažme si jednoduchý příklad, kde jak předek, tak i jeho potomek mají pouze jedinou metodu eat:

from typing import override
 
 
class Fruit:
    def eat(self):
        pass
 
class Apple(Fruit):
    @override
    def eat(self):
        pass

Statická typová kontrola by měla detekovat nekorektní použití dekorátoru (a nebo špatné jméno metody) v dalším demonstračním příkladu. Jméno metody v potomkovi se totiž v tomto zdrojovém kódu liší od jména metody předka, i když programátor dal najevo, že chce metodu překrýt:

from typing import override
 
 
class Fruit:
    def eat(self):
        pass
 
class Apple(Fruit):
    @override
    def eat_apple(self):
        pass

Zprávy vypsané všemi třemi nástroji, kterými se dnes zabýváme, vypadají takto:

$ uv run mypy --strict override-2.py
 
override-2.py:17: error: Function is missing a return type annotation  [no-untyped-def]
override-2.py:17: note: Use "-> None" if function does not return a value
override-2.py:22: error: Function is missing a return type annotation  [no-untyped-def]
override-2.py:22: note: Use "-> None" if function does not return a value
override-2.py:22: error: Method "eat_apple" is marked as an override, but no base method was found with this name  [misc]
Found 3 errors in 1 file (checked 1 source file)
$ uv run pyright override-2.py
 
/tmp/ramdisk/type-checkers/type-checkers/override-2.py
  /tmp/ramdisk/type-checkers/type-checkers/override-2.py:22:9 - error: Method "eat_apple" is marked as override, but no base method of same name is present (reportGeneralTypeIssues)
1 error, 0 warnings, 0 informations
$ uv run ty check override-2.py
 
error[invalid-explicit-override]: Method `eat_apple` is decorated with `@override` but does not override anything
  --> override-2.py:21:5
   |
21 |     @override
   |     ---------
22 |     def eat_apple(self):
   |         ^^^^^^^^^
   |
info: No `eat_apple` definitions were found on any superclasses of `Apple`
 
Found 1 diagnostic

Detekce rozdílu mezi běžným slovníkem a typem Mapping

V jazyce Python nalezneme (pochopitelně kromě mnoha dalších tříd) i třídu nazvanou collections.abc.Mapping (což je abstraktní bázová třída, jak napovídá její balíček abc). Její generickou variantu lze naimportovat i přes typing.Mapping. V obou případech se jedná o variantu neměnitelného (immutable) slovníku, tj. slovníku, který by nemělo být možné po jeho inicializaci modifikovat. Ovšem Mapping je abstraktní třídou, tj. záleží na její konkrétní implementaci. A právě v tomto ohledu se výsledky nástrojů pro statickou typovou konverzi budou odlišovat – některé nástroje budou sledovat způsob použití abstraktní třídy, jiné nástroje potom její implementaci:

from typing import Dict, Mapping
 
d1:Dict[str, float] = {}
 
d1["foo"] = 1
d1["bar"] = 3.14
d1["baz"] = 0.0
 
print(d1)
 
d2:Mapping[str, float] = {}
 
d2["foo"] = 1
d2["bar"] = 3.14
d2["baz"] = 0.0
 
print(d2)

Nástroj Mypy ohlásí nekorektní modifikaci slovníku d2:

$ uv run mypy --strict dict_mapping.py
 
dict_mapping.py:28: error: Unsupported target for indexed assignment ("Mapping[str, float]")  [index]
dict_mapping.py:29: error: Unsupported target for indexed assignment ("Mapping[str, float]")  [index]
dict_mapping.py:30: error: Unsupported target for indexed assignment ("Mapping[str, float]")  [index]
Found 3 errors in 1 file (checked 1 source file)

Oba další nástroje, tj. jak Pyright, tak i ty, ovšem v tomto zdrojovém kódu žádné problémy nenaleznou:

$ uv run pyright dict_mapping.py
 
0 errors, 0 warnings, 0 informations
$ uv run ty check dict_mapping.py
 
All checks passed!

Opačné chování: modifikace slovníku typu frozendict

Prozatím by se mohlo zdát, že nástroj Mypy dokáže najít nejvíce potenciálně problematických míst ve zdrojovém kódu. Ovšem nemusí tomu tak být ve všech případech. Pokusme se například použít třídu nazvanou frozendict ze stejnojmenného balíčku (ten je zapotřebí doinstalovat):

from frozendict import frozendict
 
d = frozendict(x=1, y=2, z="foo")
print(d)
 
d["a"] = "bar"
 
del d["z"]

Modifikace neměnného slovníku Mypy v tomto případě neodhalí (což je překvapení):

$ uv run mypy --strict frozen_dict_2.py
 
Success: no issues found in 1 source file

Naproti tomu oba další nástroje, tedy jak Pyright tak i ty upozorní na obě operace, kterými se slovník modifikuje:

$ uv run pyright frozen_dict_2.py
 
/tmp/ramdisk/type-checkers/type-checkers/frozen_dict_2.py
  /tmp/ramdisk/type-checkers/type-checkers/frozen_dict_2.py:6:1 - error: "__setitem__" method not defined on type "frozendict[str, int | str]" (reportIndexIssue)
  /tmp/ramdisk/type-checkers/type-checkers/frozen_dict_2.py:8:5 - error: "__delitem__" method not defined on type "frozendict[str, int | str]" (reportIndexIssue)
2 errors, 0 warnings, 0 informations
$ uv run ty check frozen_dict_2.py
 
error[invalid-assignment]: Cannot assign to a subscript on an object of type `frozendict[str, Literal[1, 2, "foo"]]`
 --> frozen_dict_2.py:6:1
  |
6 | d["a"] = "bar"
  | ^^^^^^
  |
info: `frozendict[str, Literal[1, 2, "foo"]]` does not have a `__setitem__` method.
 
error[not-subscriptable]: Cannot delete subscript on object of type `frozendict[str, Literal[1, 2, "foo"]]` with no `__delitem__` method
 --> frozen_dict_2.py:8:5
  |
8 | del d["z"]
  |     ^^^^^^
  |
 
Found 2 diagnostics

Modifikace množiny typu frozenset

Mohlo by se zdát, že důvodem, proč Mypy nenašel v předchozím zdrojovém kódu žádné problémy, může být fakt, že se jedná o externí balíček. Ovšem namísto frozendict můžeme použít i standardní balíček frozenset (jedná se skutečně o základní datový typ Pythonu, který není zapotřebí explicitně importovat):

class frozenset(object)
 |  frozenset(iterable=(), /)
 |
 |  Build an immutable unordered collection of unique elements.

V dalším příkladu nejprve zkonstruujeme a naplníme kontejner typu frozenset a následně se do něj pokusíme přidat další prvek:

ATTACHMENT_CONTENT_TYPES = frozenset(
    {"text/plain", "application/json", "application/yaml", "application/xml"}
)
 
ATTACHMENT_CONTENT_TYPES.add("image/png")

Nástroj Mypy neodhalí žádné problémy:

$ uv run mypy --strict frozen_set.py
 
Success: no issues found in 1 source file

Ostatní dva nástroje korektně ohlásí volání metody add, která by pro neměnitelnou množinu neměla existovat:

$ uv run pyright frozen_set.py
 
/tmp/ramdisk/type-checkers/type-checkers/frozen_set.py
  /tmp/ramdisk/type-checkers/type-checkers/frozen_set.py:5:26 - error: Cannot access attribute "add" for class "frozenset[str]"
    Attribute "add" is unknown (reportAttributeAccessIssue)
1 error, 0 warnings, 0 informations
$ uv run ty check frozen_set.py
 
error[unresolved-attribute]: Object of type `frozenset[str]` has no attribute `add`
 --> frozen_set.py:5:1
  |
5 | ATTACHMENT_CONTENT_TYPES.add("image/png")
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
 
Found 1 diagnostic

Přiřazení nové hodnoty do konstanty, modifikace konstanty

I když to zpočátku může vypadat poněkud neobvykle, je možné v Pythonu použít typ Final pro označení takových proměnných, u nichž potřebujeme zakázat možnost jejich modifikace (tedy změny reference). To tedy jinými slovy znamená, že se z takové proměnné vlastně stane konstanta, resp. objekt, který se konstantě do značné míry přibližuje.

Definice takové proměnné může vypadat následovně:

from typing import Final
 
x: Final[int] = 42
print(x)

Nástroje pro statickou typovou kontrolu by měly odhalit jakékoli pokusy o modifikaci takové proměnné. To si ostatně můžeme snadno ověřit:

from typing import Final
 
x: Final[int] = 42
print(x)
 
x = 0
print(x)

Všechny tři nástroje naleznou tu samou chybu, i když ji pochopitelně ohlásí odlišným způsobem:

$ uv run mypy --strict final_var_2.py
 
final_var_2.py:6: error: Cannot assign to final name "x"  [misc]
Found 1 error in 1 file (checked 1 source file)
$ uv run pyright final_var_2.py
 
/tmp/ramdisk/type-checkers/type-checkers/final_var_2.py
  /tmp/ramdisk/type-checkers/type-checkers/final_var_2.py:6:1 - error: "x" is declared as Final and cannot be reassigned (reportGeneralTypeIssues)
1 error, 0 warnings, 0 informations
$ uv run ty check final_var_2.py
 
error[invalid-assignment]: Reassignment of `Final` symbol `x` is not allowed
 --> final_var_2.py:3:4
  |
3 | x: Final[int] = 42
  |    ---------- Symbol declared as `Final` here
4 | print(x)
5 |
6 | x = 0
  | ^^^^^ Symbol later reassigned here
  |
 
Found 1 diagnostic

Ovšem změna obsahu proměnné může být nepřímá, například s využitím operátoru += atd.:

from typing import Final
 
x: Final[int] = 42
print(x)
 
x += 1
print(x)

I to je pochopitelně detekováno všemi třemi dnes popisovanými nástroji:

$ uv run mypy --strict final_var_3.py
 
final_var_3.py:6: error: Cannot assign to final name "x"  [misc]
Found 1 error in 1 file (checked 1 source file)
$ uv run pyright final_var_3.py
 
/tmp/ramdisk/type-checkers/type-checkers/final_var_3.py
  /tmp/ramdisk/type-checkers/type-checkers/final_var_3.py:6:1 - error: "x" is declared as Final and cannot be reassigned (reportGeneralTypeIssues)
1 error, 0 warnings, 0 informations
$ uv run ty check final_var_3.py
 
error[invalid-assignment]: Reassignment of `Final` symbol `x` is not allowed
 --> final_var_3.py:3:4
  |
3 | x: Final[int] = 42
  |    ---------- Symbol declared as `Final` here
4 | print(x)
5 |
6 | x += 1
  | ^^^^^^ Symbol later reassigned here
  |
 
Found 1 diagnostic

Detekce pokusu o modifikaci neměnitelné n-tice

Jedním ze základních datových typů v Pythonu je i n-tice (tuple), jejíž obsah je neměnitelný (immutable), na rozdíl od (zdánlivě podobného) seznamu. To ovšem znamená, že následující skript je chybný a typové kontroly by měly odhalit, že se pokoušíme provést modifikaci jednoho prvku n-tice:

x: tuple[str, ...] = ("foo", "bar", "baz")
print(x)
 
x[1] = "xyzzy"
print(x)

Ověřme si nyní chování všech tří nástrojů:

$ uv run mypy --strict tuple_2.py
 
tuple_2.py:4: error: Unsupported target for indexed assignment ("tuple[str, ...]")  [index]
Found 1 error in 1 file (checked 1 source file)
$ uv run pyright tuple_2.py
 
/tmp/ramdisk/type-checkers/type-checkers/tuple_2.py
  /tmp/ramdisk/type-checkers/type-checkers/tuple_2.py:4:1 - error: "__setitem__" method not defined on type "tuple[Literal['foo'], Literal['bar'], Literal['baz']]" (reportIndexIssue)
1 error, 0 warnings, 0 informations

A konečně:

$ uv run ty check tuple_2.py
 
error[invalid-assignment]: Cannot assign to a subscript on an object of type `tuple[Literal["foo"], Literal["bar"], Literal["baz"]]`
 --> tuple_2.py:4:1
  |
4 | x[1] = "xyzzy"
  | ^^^^
  |
info: `tuple[Literal["foo"], Literal["bar"], Literal["baz"]]` does not have a `__setitem__` method.
 
Found 1 diagnostic

Zajímavé ovšem je, že pokud se pokusíme o zavolání metody pro „rozšíření n-tice“ (taková metoda neexistuje), nebude mypy schopen tento problém detekovat:

x: tuple[str, ...] = ("foo", "bar", "baz")
print(x)
 
x.append("xyzzy")
print(x)

Mypy:

$ uv run mypy --strict tuple_3.py
 
Success: no issues found in 1 source file
Poznámka: to je ovšem poněkud nemilé překvapení.

Naproti tomu oba dva další nástroje chybu korektně naleznou a vypíšou:

$ uv run pyright tuple_3.py
 
/tmp/ramdisk/type-checkers/type-checkers/tuple_3.py
  /tmp/ramdisk/type-checkers/type-checkers/tuple_3.py:4:3 - error: Cannot access attribute "append" for class "tuple[Literal['foo'], Literal['bar'], Literal['baz']]"
    Attribute "append" is unknown (reportAttributeAccessIssue)
1 error, 0 warnings, 0 informations
$ uv run ty check tuple_3.py
 
error[unresolved-attribute]: Object of type `tuple[Literal["foo"], Literal["bar"], Literal["baz"]]` has no attribute `append`
 --> tuple_3.py:4:1
  |
4 | x.append("xyzzy")
  | ^^^^^^^^
  |
 
Found 1 diagnostic

Problém variance v typovém systému Pythonu

„Argument types must be contra-variant, return types must be co-variant.“

Mezi jednotlivými typovými systémy může být velké množství rozdílů, které určují jak jejich sílu, tak i snadnost nebo naopak komplikace při použití. Zajímavou vlastností typových systémů je takzvaná typová variance (do češtiny se sice překládá několika termíny, ovšem pravděpodobně bude lepší zůstat u původního označení). U datových typů (a odvozeně i od třídní hierarchie) typicky vyžadujeme tuto vlastnost: nějaký typ T je podtypem dalšího typu U tehdy, když ve všech místech programového kódu, v nichž je očekávána hodnota typu U, můžeme použít i hodnotu typu T (v případě třídního objektově orientovaného programování známe podobnou vlastnost: „potomek vždy může nahradit předka“, která ovšem vychází z dědičnosti. Dědičnost je ovšem nezávislá vlastnost.).

Variance určuje, jak a zda vůbec se toto pravidlo uplatní například u polí, resp. v případě Pythonu u seznamů (List[T] versus List[U]), map, a jak později uvidíme, tak i funkcí (typy parametrů a typ návratové hodnoty) atd.

Celkem existují čtyři varianty:

  1. Covariance
  2. Contravariance
  3. Invariance
  4. Bivariance
Poznámka: opět používám původní termíny, které se lépe vyhledávají.

Podívejme se nyní na základní vlastnosti jednotlivých variant variancí. Budeme předpokládat, že máme nadefinovány dva datové typy nazvané Ovoce a Hruška, přičemž Hruška je podtypem typu Ovoce:

  1. Covariance: List[Hruška] je podtypem typu List[Ovoce], funkce akceptující List[Ovoce] bude akceptovat List[Hruška]
  2. Contravariance: List[Ovoce] je podtypem typu List[Hruška] (což je jen zdánlivě nelogické), funkce akceptující List[Hruška] bude akceptovat List[Ovoce]
  3. Invariance: List[Ovoce] nemá vztah k List[Hruška] a při volání funkcí je tedy nelze zaměňovat
  4. Bivariance>: List[Hruška] je podtypem typu List[Ovoce] a současně List[Ovoce] je podtypem typu List[Hruška], jsou tedy při volání zaměnitelné
Poznámka: může se to možná zdát divné, ale všechny typy variance mají svůj význam i použití, i když se mohou zdát na první pohled „nelogické“. Ostatně příklady si ukážeme v navazujícím textu. Situace je zajímavá zejména u funkcí, resp. u jejich parametrů a návratové hodnoty, protože právě zde se uplatňuje zdánlivě nelogická kontravariance.

Ukažme si nejdříve zcela korektní demonstrační příklad s třídami Ovoce, Hruska a Jablko i s funkcí smíchej, které se předá seznam, jenž může obsahovat libovolnou kombinaci ovoce:

from typing import List
 
 
class Ovoce:
    pass
 
 
class Hruska(Ovoce):
    def __repr__(self) -> str:
        return "Hruska"
 
 
class Jablko(Ovoce):
    def __repr__(self) -> str:
        return "Jablko"
 
 
def smichej(kosik : List[Ovoce]) -> None:
    kosik.append(Hruska())
    kosik.append(Jablko())
 
 
kosik : List[Ovoce] = []
 
smichej(kosik)
 
for ovoce in kosik:
    print(ovoce)

V tomto kódu nejsou nalezeny žádné typově specifické problémy:

$ uv run mypy --strict variance1.py
 
Success: no issues found in 1 source file
$ uv run pyright variance1.py
 
0 errors, 0 warnings, 0 informations
$ uv run ty check variance1.py
 
All checks passed!

Nyní předchozí příklad nepatrně pozměníme – košík nebude typu List[Ovoce] ale typu List[Hruska], přičemž ve funkci smichej opět mícháme jablka s hruškami, ovšem přidáváme je do seznamu, který akceptuje pouze hrušky:

from typing import List
 
 
class Ovoce:
    pass
 
 
class Hruska(Ovoce):
    def __repr__(self) -> str:
        return "Hruska"
 
 
class Jablko(Ovoce):
    def __repr__(self) -> str:
        return "Jablko"
 
 
def smichej(kosik : List[Ovoce]) -> None:
    kosik.append(Hruska())
    kosik.append(Jablko())
 
 
kosik : List[Hruska] = []
 
smichej(kosik)
 
for ovoce in kosik:
    print(ovoce)

Zcela korektně se nyní budou hlásit typově specifické problémy (povšimněte si rozdílu oproti klasickému OOP a jeho základním poučkám):

$ uv run mypy --strict variance2.py
 
variance2.py:40: error: Argument 1 to "smichej" has incompatible type "list[Hruska]"; expected "list[Ovoce]"  [arg-type]
variance2.py:40: note: "list" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
variance2.py:40: note: Consider using "Sequence" instead, which is covariant
Found 1 error in 1 file (checked 1 source file)
$ uv run pyright variance2.py
 
/tmp/ramdisk/type-checkers/type-checkers/variance2.py
  /tmp/ramdisk/type-checkers/type-checkers/variance2.py:40:9 - error: Argument of type "List[Hruska]" cannot be assigned to parameter "kosik" of type "List[Ovoce]" in function "smichej"
    "List[Hruska]" is not assignable to "List[Ovoce]"
      Type parameter "_T@list" is invariant, but "Hruska" is not the same as "Ovoce"
      Consider switching from "list" to "Sequence" which is covariant (reportArgumentType)
1 error, 0 warnings, 0 informations
$ uv run ty check variance2.py
 
error[invalid-argument-type]: Argument to function `smichej` is incorrect
  --> variance2.py:40:9
   |
40 | smichej(kosik)
   |         ^^^^^ Expected `list[Ovoce]`, found `list[Hruska]`
   |
info: Function defined here
  --> variance2.py:33:5
   |
33 | def smichej(kosik : List[Ovoce]) -> None:
   |     ^^^^^^^ ------------------- Parameter declared here
   |
info: `list` is invariant in its type parameter
info: Consider using the covariant supertype `collections.abc.Sequence`
info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics
 
Found 1 diagnostic

Zkusme si příklad zjednodušit, a to tak, že vytvoříme funkci nazvanou tiskni, která očekává jako svůj parametr typ List[Ovoce]. A vyzkoušíme si, zda je možné této funkci předat seznam s hruškami, tedy List[Hruska]. Při statické kontrole typů by se měl objevit stejný problém, jako u předchozího příkladu (protože se kontrolují jen typy a Mypy ani další dva nástroje netuší, jaké operace se budou interně se seznamem provádět):

from typing import List
 
 
class Ovoce:
    pass
 
 
class Hruska(Ovoce):
    def __repr__(self) -> str:
        return "Hruska"
 
 
class Jablko(Ovoce):
    def __repr__(self) -> str:
        return "Jablko"
 
 
def tiskni(kosik : List[Ovoce]) -> None:
    for ovoce in kosik:
        print(ovoce)
 
 
kosik : List[Hruska] = []
 
tiskni(kosik)

Výsledky provedení statických typových kontrol jsou vypsány pod tímto odstavcem. Všechny tři nástroje naleznou stejnou chybu, ovšem samotná chybová hlášení jsou značně odlišná:

$ uv run mypy --strict variance3.py
 
variance3.py:40: error: Argument 1 to "tiskni" has incompatible type "list[Hruska]"; expected "list[Ovoce]"  [arg-type]
variance3.py:40: note: "list" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
variance3.py:40: note: Consider using "Sequence" instead, which is covariant
Found 1 error in 1 file (checked 1 source file)
$ uv run pyright variance3.py
 
/tmp/ramdisk/type-checkers/type-checkers/variance3.py
  /tmp/ramdisk/type-checkers/type-checkers/variance3.py:40:8 - error: Argument of type "List[Hruska]" cannot be assigned to parameter "kosik" of type "List[Ovoce]" in function "tiskni"
    "List[Hruska]" is not assignable to "List[Ovoce]"
      Type parameter "_T@list" is invariant, but "Hruska" is not the same as "Ovoce"
      Consider switching from "list" to "Sequence" which is covariant (reportArgumentType)
1 error, 0 warnings, 0 informations
$ uv run ty check variance3.py
 
error[invalid-argument-type]: Argument to function `tiskni` is incorrect
  --> variance3.py:40:8
   |
40 | tiskni(kosik)
   |        ^^^^^ Expected `list[Ovoce]`, found `list[Hruska]`
   |
info: Function defined here
  --> variance3.py:33:5
   |
33 | def tiskni(kosik : List[Ovoce]) -> None:
   |     ^^^^^^ ------------------- Parameter declared here
   |
info: `list` is invariant in its type parameter
info: Consider using the covariant supertype `collections.abc.Sequence`
info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics
 
Found 1 diagnostic

Jak nástroje správně napovídají, lze zdrojový kód předchozího příkladu upravit takovým způsobem, že se namísto datového typu List (což odpovídá klasickému Pythonovskému seznamu) použije typ Sequence, který představuje neměnný (immutable) seznam. A právě díky této úpravě – kdy bude i Mypy během statické analýzy kódu vědět, že do seznamu nelze vložit hodnotu nesprávného typu – bude příklad typově korektní:

from typing import Sequence
 
 
class Ovoce:
    pass
 
 
class Hruska(Ovoce):
    def __repr__(self) -> str:
        return "Hruska"
 
 
class Jablko(Ovoce):
    def __repr__(self) -> str:
        return "Jablko"
 
 
def tiskni(kosik : Sequence[Ovoce]) -> None:
    for ovoce in kosik:
        print(ovoce)
 
 
kosik : Sequence[Hruska] = []
 
tiskni(kosik)
$ uv run mypy --strict variance4.py 
 
Success: no issues found in 1 source file
$ uv run pyright variance4.py
 
0 errors, 0 warnings, 0 informations

A konečně:

$ uv run ty check variance4.py
 
All checks passed!

Rychlost provádění statických typových kontrol

Již v perexu tohoto článku jsme se zmínili o tom, že Mypy, Pyright a ty se od sebe odlišují i svou rychlostí. Ta sice nemusí být kritická pro menší projekty, ovšem u projektů rozsáhlejších (navíc se závislostmi) již může statická typová kontrola zabrat poměrně velké množství času. Rychlost si ověříme na středně rozsáhlém projektu Lightspeed Stack:

$ time uv run mypy --strict src
 
real    0m19.913s
user    0m18.845s
sys     0m0.924s
$ time uv run pyright src
 
real    0m14.016s
user    0m31.460s
sys     0m0.877s
$ time uv run ty check src
 
real    0m0.614s
user    0m3.961s
sys     0m0.574s

Z vypsaných časů je zřejmé, že zdaleka nejrychlejší je nástroj ty, zatímco Mypy i Pyright jsou mnohem pomalejší. Rozdíly v rychlostech jednotlivých nástrojů budou ještě lépe patrné, pokud si je zobrazíme do grafu:

import matplotlib.pyplot as plt
 
 
tools = ["Mypy", "Pyright", "ty"]
times = [18.8, 32.46, 3.96]
 
plt.xlabel("Tool")
plt.ylabel("Time (sec)")
plt.bar(tools, times)
 
# přidání legendy
plt.legend(loc="upper left")
 
# povolení zobrazení mřížky
#plt.grid(True)
 
plt.savefig("benchmark.png")
 
# zobrazení grafu
plt.show()

Výsledný graf ještě lépe ukazuje, jak je ty rychlý:

TOP100

Porovnání rychlostí nástrojů pro statické typové kontroly.

Obrázek 1: Porovnání rychlostí nástrojů pro statické typové kontroly (nižší sloupec = vyšší rychlost). 

Autor: tisnik, podle licence: Rights Managed

Poznámka: v praxi však může být výhodné alespoň na CI spouštět všechny tři nástroje, protože se dobře doplňují. „Nejpečlivější“ kontroly provádí Pyright, ovšem na druhou stranu jsou jeho chybová hlášení poměrně špatně dešifrovatelná.

Repositář s demonstračními příklady

Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Příklad Stručný popis Adresa příkladu
1 pyproject.toml projektový soubor používaný všemi demonstračními příklady https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/pyproject.toml
       
2 final_var1.py definice finální proměnné (konstanty) https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_var1.py
3 final_var2.py pokus o přiřazení do finální proměnné https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_var2.py
4 final_var3.py pokus o modifikace hodnoty finální proměnné https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_var3.py
       
5 list1.py heterogenní seznam https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/list1.py
6 list2.py homogenní seznam https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/list2.py
7 list3.py modifikace prvku seznamu https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/list3.py
       
8 final_list1.py přiřazení nové hodnoty do finální proměnné https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_list1.py
9 final_list2.py přímá modifikace prvku finálního seznamu https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_list2.py
10 final_list3.py přidání nového prvku do finálního seznamu https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_list3.py
       
11 tuple1.py definice n-tice https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/tuple1.py
12 tuple2.py pokus o modifikaci prvku n-tice https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/tuple2.py
13 tuple3.py pokus o přidání nového prvku do n-tice https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/tuple3.py
       
14 final_tuple1.py definice n-tice (finální proměnná) https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_tuple1.py
15 final_tuple2.py pokus o modifikaci prvku finální n-tice https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_tuple2.py
16 final_tuple3.py pokus o přidání nového prvku do finální n-tice https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/final_tuple3.py
       
17 frozen_dict1.py využití datového kontejneru frozendict https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/frozen_dict1.py
18 frozen_dict2.py modifikace datového kontejneru frozendict https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/frozen_dict2.py
19 frozen_dict3.py modifikace datového kontejneru frozendict https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/frozen_dict3.py
       
20 adder1.py funkce add bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/adder1.py
21 adder2.py funkce add s typovými anotacemi https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/adder2.py
22 adder3.py funkce add volaná s hodnotami TrueFalse https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/adder3.py
23 adder4.py funkce add akceptující hodnoty typu bool https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/adder4.py
       
24 appender1.py funkce append bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/appender1.py
25 appender2.py funkce append s typovými anotacemi, korektní volání metod https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/appender2.py
26 appender3.py funkce append s typovými anotacemi, nekorektní volání metod https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/appender3.py
       
27 callable1.py funkce s typovými informacemi https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/callable1.py
28 callable2.py variance funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/callable2.py
29 callable3.py variance funkcí (nekorektní příklad) https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/callable3.py
30 callable4.py korektní řešení problému z kódu callable3.py https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/callable4.py
       
31 dict_mapping.py měnitelné slovníky vs. neměnitelné mapování https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/dict_mapping.py
32 frozen_set.py použití hodnoty typu frozenset https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/frozen_set.py
       
33 override-1.py dekorátor @override, varianta určená pro Python 3.12 https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/override-1.py
34 override-2.py dekorátor @override, nekorektní použití https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/override-2.py
35 override-3.py dekorátor @override, varianta určená pro Mypy https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/override-3.py
36 override-4.py dekorátor @override, metody s typy, varianta určená pro Mypy https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/override-4.py
       
37 queue.py použití hodnoty typu queue https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/queue.py
38 deque.py použití hodnoty typu deque https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/deque.py
       
39 variance1.py variance v Pythonu – korektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/variance1.py
40 variance2.py variance v Pythonu – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/variance2.py
41 variance3.py variance v Pythonu – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/variance3.py
42 variance4.py použití typu Sequence namísto List https://github.com/tisnik/most-popular-python-libs/blob/master/type-checkers/variance4.py

Odkazy na další články o typovém systému Pythonu

S typovými anotacemi funkcí (i dalších hodnot) pracuje například nástroj Mypy, s nímž jsme se již setkali v následujících článcích:

  1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy
    https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy/
  2. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (2.část)
    https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-2-cast/
  3. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (3)
    https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-3/
  4. Novinky v typovém systému přidané do Pythonu 3.12
    https://www.root.cz/clanky/novinky-v-typovem-systemu-pridane-do-pythonu-3–12/

Odkazy na Internetu

  1. ty – An extremely fast Python type checker and language server, written in Rust
    https://docs.astral.sh/ty/
  2. PEP 698 – Override Decorator for Static Typing
    https://peps.python.org/pep-0698/
  3. typing.override
    https://docs.python.org/3/li­brary/typing.html#typing.o­verride
  4. Type Hinting
    https://realpython.com/lessons/type-hinting/
  5. mypy homepage
    https://www.mypy-lang.org/
  6. mypy documentation
    https://mypy.readthedocs.i­o/en/stable/
  7. Mypy na PyPi Optional static typing for Python
    https://pypi.org/project/mypy/
  8. 5 Reasons Why You Should Use Type Hints In Python
    https://www.youtube.com/wat­ch?v=dgBCEB2jVU0
  9. Python Typing – Type Hints & Annotations
    https://www.youtube.com/watch?v=QORvB-_mbZ0
  10. What Problems Can TypeScript Solve?
    https://www.typescriptlang.org/why-create-typescript
  11. How to find code that is missing type annotations?
    https://stackoverflow.com/qu­estions/59898490/how-to-find-code-that-is-missing-type-annotations
  12. Do type annotations in Python enforce static type checking?
    https://stackoverflow.com/qu­estions/54734029/do-type-annotations-in-python-enforce-static-type-checking
  13. Understanding type annotation in Python
    https://blog.logrocket.com/un­derstanding-type-annotation-python/
  14. Static type checking with Mypy — Perfect Python
    https://www.youtube.com/wat­ch?v=9gNnhNxra3E
  15. Static Type Checker for Python
    https://github.com/microsoft/pyright
  16. Differences Between Pyright and Mypy
    https://github.com/microsof­t/pyright/blob/main/docs/my­py-comparison.md
  17. 4 Python type checkers to keep your code clean
    https://www.infoworld.com/ar­ticle/3575079/4-python-type-checkers-to-keep-your-code-clean.html
  18. Pyre: A performant type-checker for Python 3
    https://pyre-check.org/
  19. „Typing the Untyped: Soundness in Gradual Type Systems“ by Ben Weissmann
    https://www.youtube.com/wat­ch?v=uJHD2×yv7×o
  20. Covariance and contravariance (computer science)
    https://en.wikipedia.org/wi­ki/Covariance_and_contrava­riance_(computer_science)
  21. Functional Programming: Type Systems
    https://www.youtube.com/wat­ch?v=hy1wjkcIBCU
  22. A Type System From Scratch – Robert Widmann
    https://www.youtube.com/wat­ch?v=IbjoA5×VUq0
  23. „Type Systems – The Good, Bad and Ugly“ by Paul Snively and Amanda Laucher
    https://www.youtube.com/wat­ch?v=SWTWkYbcWU0
  24. Type Systems: Covariance, Contravariance, Bivariance, and Invariance explained
    https://medium.com/@thejameskyle/type-systems-covariance-contravariance-bivariance-and-invariance-explained-35f43d1110f8
  25. Statická vs. dynamická typová kontrola
    https://www.root.cz/clanky/staticka-dynamicka-typova-kontrola/
  26. Typový systém
    https://cs.wikipedia.org/wi­ki/Typov%C3%BD_syst%C3%A9m
  27. Comparison of programming languages by type system
    https://en.wikipedia.org/wi­ki/Comparison_of_programmin­g_languages_by_type_system
  28. Persistent data structure
    https://en.wikipedia.org/wi­ki/Persistent_data_structu­re
  29. Collections (Python)
    https://docs.python.org/3/li­brary/collections.abc.html
  30. Immutable object
    https://en.wikipedia.org/wi­ki/Immutable_object
  31. pyrsistent na PyPi
    https://pypi.org/project/pyrsistent/
  32. pyrsistent na GitHubu
    https://github.com/tobgu/pyrsistent
  33. Dokumentace knihovny pyrsistent
    https://pyrsistent.readthe­docs.io/en/latest/index.html
  34. pyrthon na GitHubu
    https://github.com/tobgu/pyrthon/
  35. Mori na GitHubu
    https://github.com/swannodette/mori
  36. Mori: popis API (dokumentace)
    http://swannodette.github.io/mori/
  37. Mori: Benchmarking
    https://github.com/swanno­dette/mori/wiki/Benchmarking
  38. Functional data structures in JavaScript with Mori
    http://sitr.us/2013/11/04/functional-data-structures.html
  39. Immutable.js
    https://facebook.github.io/immutable-js/
  40. Understanding Clojure's Persistent Vectors, pt. 1
    http://hypirion.com/musin­gs/understanding-persistent-vector-pt-1
  41. Hash array mapped trie (Wikipedia)
    https://en.wikipedia.org/wi­ki/Hash_array_mapped_trie
  42. Java theory and practice: To mutate or not to mutate?
    http://www.ibm.com/develo­perworks/java/library/j-jtp02183/index.html
  43. Efficient persistent (immutable) data structures
    https://persistent.codeplex.com/
  44. Clojure (Wikipedia EN)
    http://en.wikipedia.org/wiki/Clojure
  45. Clojure (Wikipedia CS)
    http://cs.wikipedia.org/wiki/Clojure
  46. How do Python type checkers compare?
    https://pydevtools.com/han­dbook/explanation/how-do-mypy-pyright-and-ty-compare/
  47. Type System Conformance
    https://github.com/python/ty­ping/tree/main/conformance
  48. How Well Do New Python Type Checkers Conform?
    https://sinon.github.io/future-python-type-checkers/
Neutrální ikona do widgetu na odběr článků ze seriálů

Zajímá vás toto téma? Chcete se o něm dozvědět víc?

Objednejte si upozornění na nově vydané články do vašeho mailu. Žádný článek vám tak neuteče.


Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.



Nejnovější články