Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy

15. 6. 2023
Doba čtení: 30 minut

Sdílet

 Autor: Python
Nástroj Mypy se společně s dalšími podobnými nástroji (Pyright, Pyro) používá pro statickou typovou kontrolu zdrojových kódů v Pythonu. Ten totiž podporuje zápis typových anotací resp. nápověd (hints).

Obsah

1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy

2. Statické typové kontroly

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

4. Instalace nástroje Mypy

5. Datový typ Any, přidání explicitních informací o typech

6. Způsob překladu informace o datových typech do bajtkódu Pythonu

7. Utilita pro výpis struktury bajtkódu

8. Výsledek překladu do bajtkódu pro kód bez typových informací i s typovými informacemi

9. Vztah typu bool a int

10. Datový typ seznam

11. Rozdíly mezi Pythonem 3.10 a předchozími verzemi

12. Datový typ n-tice

13. Zobrazení typových anotací funkcí

14. Kdy statická typová kontrola selže?

15. Rozdíly v typových systémech: variance

16. Jak je tomu v Javě?

17. Chování Pythonu při práci se seznamy s typem

18. Použití typu Sequence namísto typu List

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

20. Odkazy na Internetu

1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy

„If it walks like a duck, and talks like a duck, then it is a duck“

V dnešním článku o programovacím jazyku Python se seznámíme se základními vlastnostmi nástroje nazvaného Mypy. Tento nástroj, společně s dalšími podobně koncipovanými nástroji, mezi něž patří Pyright a Pyro, provádí statickou typovou kontrolu zdrojových kódů napsaných v Pythonu. Python samotný totiž od verze 3.5 podporuje zápis takzvaných typových anotací resp. 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. Pochopitelně ovšem stále platí, že statická analýza nebude a nemůže být v případě Pythonu stoprocentně úspěšná, což si ostatně taktéž ukážeme. Ve stručnosti se zmíníme i o problematice tzv. variancí, což jsou pravidla určující vztahy mezi dvojicí datových typů (zda a v jakém případě je jeden z typů podtypem typu druhého). Variance, přesněji řečeno kontravariance se nejviditelněji uplatňuje u funkcí (typ Callable).

Logo nástroje Mypy naznačuje použitou syntaxi při zápisu datových typů.

Poznámka: zavedení typových anotací do Pythonu bylo provázeno určitou nelibostí a někdy též obavou o to, zda se z Pythonu nestává zcela odlišný programovací jazyk. Ovšem jedná se o zcela nepovinnou součást jazyka, která je vlastně do značné míry samotným Pythonem ignorována a i nástroj Mypy pracuje zcela nezávisle na interpretru Pythonu. Na druhou stranu, i přes uvedené rozpaky, právě typové anotace a jejich kontrola (a zpracování integrovanými vývojovými prostředími) usnadňují vývoj rozsáhlejších aplikací (a můžeme zde vidět souvislost s dvojicí JavaScript:TypeScript).

2. Statické typové kontroly

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č, 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 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 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 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č. 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).

3. 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 typy:

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 na Python, JavaScript a Ruby. Tyto jazyky se původně používaly na krátké skripty (v případě JavaScriptu běžící 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, Pyre
Ruby Sorbet

V dnešním článku se zaměříme na nástroj Mypy určený pro statickou typovou kontrolu zdrojových kódů psaných v Pythonu, ovšem současně si budeme muset popsat i základní vlastnosti typových anotací, které je možné v Pythonu používat (od verze 3.5, ve verzi 3.10 pak došlo k vylepšením). 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, ale nejsou striktně vyžadovány (a u krátkých skriptů podle mého názoru většinou i postrádají smysl).

4. Instalace nástroje Mypy

Nástroj Mypy není součástí standardní instalace Pythonu a proto ho je nutné explicitně nainstalovat. Samotná instalace je snadná (předpokladem je, že je již nainstalován Python 3.5 či vyšší):

$ pip3 install --user mypy
 
Collecting mypy
  Downloading mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.2 MB)
     |████████████████████████████████| 12.2 MB 779 kB/s
Requirement already satisfied: typing-extensions>=3.10 in ./.local/lib/python3.8/site-packages (from mypy) (4.4.0)
Collecting tomli>=1.1.0; python_version < "3.11"
  Downloading tomli-2.0.1-py3-none-any.whl (12 kB)
Collecting mypy-extensions>=1.0.0
  Downloading mypy_extensions-1.0.0-py3-none-any.whl (4.7 kB)
Installing collected packages: tomli, mypy-extensions, mypy
Successfully installed mypy-1.3.0 mypy-extensions-1.0.0 tomli-2.0.1

Většinou je ovšem ještě nutné provést upgrade balíčku nazvaného typing_extensions:

$ pip3 install --upgrade --user typing_extensions
 
Collecting typing_extensions
  Downloading typing_extensions-4.6.3-py3-none-any.whl (31 kB)
Installing collected packages: typing-extensions
  Attempting uninstall: typing-extensions
    Found existing installation: typing-extensions 4.4.0
    Uninstalling typing-extensions-4.4.0:
      Successfully uninstalled typing-extensions-4.4.0
Successfully installed typing-extensions-4.6.3

Otestování, zda je Mypy spustitelný:

$ mypy --version
 
mypy 1.3.0 (compiled: yes)

Pochopitelně si můžeme nechat zobrazit i (dlouhou) nápovědu:

$ 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:
...
...
...
Running code:
  Specify the code you want to type check. For more details, see
  mypy.readthedocs.io/en/stable/running_mypy.html#running-mypy
 
  --explicit-package-bases  Use current directory and MYPYPATH to determine
                            module names of files passed (inverse: --no-
                            explicit-package-bases)
  --exclude PATTERN         Regular expression to match file names, directory
                            names or paths which mypy should ignore while
                            recursively discovering files to check, e.g.
                            --exclude '/setup\.py$'. May be specified more
                            than once, eg. --exclude a --exclude b
  -m MODULE, --module MODULE
                            Type-check module; can repeat for more modules
  -p PACKAGE, --package PACKAGE
                            Type-check package recursively; can be repeated
  -c PROGRAM_TEXT, --command PROGRAM_TEXT
                            Type-check program passed in as string
  files                     Type-check given files or directories
 
Environment variables:
  Define MYPYPATH for additional module search path entries.
  Define MYPY_CACHE_DIR to override configuration cache_dir path.
Poznámka: základní vlastnosti Mypy si můžete otestovat i v on-line editoru dostupného na adrese https://mypy-play.net/?mypy=latest&python=3.11. Ten umožňuje výběr konkrétní verze Pythonu atd.

5. Datový typ Any, přidání explicitních informací o typech

Základní vlastnosti nástroje si můžeme otestovat na triviálním příkladu – funkci, která sečte své dva parametry, přičemž termín „sečte“ má v tomto kontextu mnoho významů („spojí“ atd.) kvůli tomu, že funkce nijak neomezuje typy parametrů ani typ návratové hodnoty:

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

Výsledek statické kontroly nástrojem Mypy může být možná poněkud překvapující, protože se nenalezne žádný problém:

$ mypy adder1.py
 
Success: no issues found in 1 source file

Je tomu tak z toho důvodu, že si Mypy „domyslí“, že parametry neudaného typu i návratové hodnoty bez zadaného typu mají ve skutečnosti typ Any.

Ovšem můžeme taktéž povolit striktní typovou kontrolu, která již tuto zkratku neumožní:

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

Zkusme si nyní kód funkce upravit tak, aby byla silně typovaná. Tj. explicitně budeme specifikovat jak typy obou parametrů, tak i typ návratové hodnoty funkce. Povšimněte si způsobu zápisu:

def add(a:int, b:int) -> int:
    return a+b

Nyní proběhne statická typová kontrola (opět) bez chyby:

$ mypy adder2.py
 
Success: no issues found in 1 source file

A dokonce ani striktní typová kontrola již žádnou chybu nenalezne:

$ mypy --strict adder2.py
 
Success: no issues found in 1 source file

6. Způsob překladu informace o datových typech do bajtkódu Pythonu

V dalším textu si ukážeme, jakým způsobem se vlastně informace o datových typech zařadí do bajtkódu Pythonu. Nejprve si vynutíme překlad obou výše uvedených příkladů (první bez explicitní specifikace typů, druhý se specifikací typů) do souborů s koncovkou .pyc, které obsahují bajtkód příkladů:

$ python3 -m compileall adder1.py
 
Compiling 'adder1.py'...

a:

$ python3 -m compileall adder2.py
 
Compiling 'adder2.py'...

Výsledkem by měla být dvojice souborů uložená v nově vytvořeném podadresáři „__pycache__“:

$ ls -l __pycache__/
 
total 24
-rw-rw-r-- 1 ptisnovs ptisnovs 200 Jun 13 08:47 adder1.cpython-38.pyc
-rw-rw-r-- 1 ptisnovs ptisnovs 235 Jun 13 08:47 adder2.cpython-38.pyc

7. Utilita pro výpis struktury bajtkódu

Pro výpis struktury bajtkódu, tedy obsahu souborů s koncovkou .pyc, kupodivu neexistuje žádný standardní nástroj. Z tohoto důvodu využijeme skript, jehož základy jsem zkopíroval ze StackOverflow (což dělám tak jednou za rok :-) a upravil takovým způsobem, aby se vypisoval i obsah constant poolu:

# Based on code snippets mentioned on following page:
# https://stackoverflow.com/questions/11141387/given-a-python-pyc-file-is-there-a-tool-that-let-me-view-the-bytecode
 
# Original authors:
# https://stackoverflow.com/users/6003870/padymko
# https://stackoverflow.com/users/5065946/powersource97
 
import sys
import struct
import marshal
import binascii
import time
import dis
import platform
import types
 
def view_pyc_file(path):
    """Read and display a content of the Python`s bytecode in a pyc-file."""
 
    with open(path, 'rb') as file:
 
        magic = file.read(4)
        bit_field = None
        timestamp = None
        hashstr = None
        size = None
 
        if sys.version_info.major == 3 and sys.version_info.minor >= 7:
            bit_field = int.from_bytes(file.read(4), byteorder=sys.byteorder)
            if 1 & bit_field == 1:
                hashstr = file.read(8)
            else:
                timestamp = file.read(4)
                size = file.read(4)
                size = struct.unpack('I', size)[0]
        elif sys.version_info.major == 3 and sys.version_info.minor >= 3:
            timestamp = file.read(4)
            size = file.read(4)
            size = struct.unpack('I', size)[0]
        else:
            timestamp = file.read(4)
 
        code = marshal.load(file)
 
    magic = binascii.hexlify(magic).decode('utf-8')
    timestamp = time.asctime(time.localtime(struct.unpack('I', timestamp)[0]))
 
    dis.disassemble(code)
 
    print('-' * 80)
 
    # print constant pool
    for i, const in enumerate(code.co_consts):
        print(i, "\t"+str(const) + "\t" + str(type(const)))
        # constant pool contains function/method bytecode, so let's try to disassemble it
        try:
            dis.disassemble(const)
        except:
            pass
 
    print('-' * 80)
    print(
        'Python version: {}\nMagic code: {}\nTimestamp: {}\nSize: {}\nHash: {}\nBitfield: {}'
        .format(platform.python_version(), magic, timestamp, size, hashstr, bit_field)
    )
 
if __name__ == '__main__':
    view_pyc_file(sys.argv[1])

8. Výsledek překladu do bajtkódu pro kód bez typových informací i s typovými informacemi

Zkusme tedy prozkoumat bajtkód obou variant funkcí add. Nejprve se podívejme na první variantu, v níž nejsou explicitně uvedeny žádné typové informace:

  1           0 LOAD_CONST               0 (<code object add at 0x7f90e76385b0, file "adder1.py", line 1>)
              2 LOAD_CONST               1 ('add')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (add)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE
--------------------------------------------------------------------------------
0        <code object add at 0x7f90e76385b0, file "adder1.py", line 1>
  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE
1        add
2        None
--------------------------------------------------------------------------------
Python version: 3.8.10
Magic code: 550d0d0a
Timestamp: Thu Jun  8 15:36:13 2023
Size: 30
Hash: None
Bitfield: 0
Poznámka: povšimněte si, že instrukce tvořící tělo funkce add jsou umístěny jako konstanta (s indexem 0), ovšem zajímavější je zařazení funkce do skriptu: jedná se o první tři instrukce bajtkódu, které pouze načtou bajtkód funkce, její jméno a vytvoří z této dvojice novou hodnotu typu „funkce“:
  1           0 LOAD_CONST               0 (<code object add at 0x7f90e76385b0, file "adder1.py", line 1>)
              2 LOAD_CONST               1 ('add')
              4 MAKE_FUNCTION            0

Druhá varianta:

  1           0 LOAD_NAME                0 (int)
              2 LOAD_NAME                0 (int)
              4 LOAD_NAME                0 (int)
              6 LOAD_CONST               0 (('a', 'b', 'return'))
              8 BUILD_CONST_KEY_MAP      3
             10 LOAD_CONST               1 (<code object add at 0x7ff3987475b0, file "adder2.py", line 1>)
             12 LOAD_CONST               2 ('add')
             14 MAKE_FUNCTION            4 (annotations)
             16 STORE_NAME               1 (add)
             18 LOAD_CONST               3 (None)
             20 RETURN_VALUE
--------------------------------------------------------------------------------
0        ('a', 'b', 'return')
1        <code object add at 0x7ff3987475b0, file "adder2.py", line 1>
  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE
2        add
3        None
--------------------------------------------------------------------------------
Python version: 3.8.10
Magic code: 550d0d0a
Timestamp: Thu Jun  8 15:36:25 2023
Size: 45
Hash: None
Bitfield: 0

Pro druhou variantu funkce add můžeme vidět naprosto stejný bajtkód (tedy tělo funkce zůstává nezměněno!), ovšem mění se způsob definice funkce – před instrukci MAKE_FUNCTION je umístěna instrukce BUILD_CONST_KEY_MAP, která zpracuje informace o typech:

  1           0 LOAD_NAME                0 (int)
              2 LOAD_NAME                0 (int)
              4 LOAD_NAME                0 (int)
              6 LOAD_CONST               0 (('a', 'b', 'return'))
              8 BUILD_CONST_KEY_MAP      3

Druhou část již známe, ovšem je zde jedna změna – MAKE_FUNCTION nyní vytvoří funkci s typovými anotacemi:

             10 LOAD_CONST               1 (<code object add at 0x7ff3987475b0, file "adder2.py", line 1>)
             12 LOAD_CONST               2 ('add')
             14 MAKE_FUNCTION            4 (annotations)
Poznámka: opět zdůrazním – tělo funkce se nezmění, proto v runtime bude funkce stále akceptovat parametry libovolného typu!

9. Vztah typu bool a int

V programovacím jazyku Python existuje (na tak vysokoúrovňový jazyk) poněkud zvláštní vztah mezi datovými typy bool a int. Hodnoty True a False jsou totiž považovány za ekvivalentní hodnotám 1 a 0, což například znamená, že následující demonstrační příklad bude vyhodnocen jako korektní z hlediska použití datových typů:

def add(a:int, b:int) -> int:
    return a+b
 
print(add(1, 2))
print(add(1, True))
print(add(1, False))

Jak běžná, tak i striktní typová kontrola neodhalí v tomto kódu žádné problémy:

$ mypy adder3.py
 
Success: no issues found in 1 source file
 
 
 
$ mypy --strict adder3.py
 
Success: no issues found in 1 source file

Zajímavé bude zjistit, jak vlastně vypadá bajtkód a tabulka konstant v tomto příkladu. Nejprve si tedy vynutíme jeho překlad do bajtkódu:

$ python3 -m compileall adder3.py
 
Compiling 'adder3.py'...

Výsledek bude vypadat následovně. Povšimněte si, že se při volání funkce hodnoty předávaných parametrů načítají do zásobníku instrukcí LOAD_CONST, takže důležitý je obsah tabulky konstant:

  1           0 LOAD_NAME                0 (int)
              2 LOAD_NAME                0 (int)
              4 LOAD_NAME                0 (int)
              6 LOAD_CONST               0 (('a', 'b', 'return'))
              8 BUILD_CONST_KEY_MAP      3
             10 LOAD_CONST               1 (<code object add at 0x7f413d3d25b0, file "adder3.py", line 1>)
             12 LOAD_CONST               2 ('add')
             14 MAKE_FUNCTION            4 (annotations)
             16 STORE_NAME               1 (add)
 
  4          18 LOAD_NAME                2 (print)
             20 LOAD_NAME                1 (add)
             22 LOAD_CONST               3 (1)
             24 LOAD_CONST               4 (2)
             26 CALL_FUNCTION            2
             28 CALL_FUNCTION            1
             30 POP_TOP
 
  5          32 LOAD_NAME                2 (print)
             34 LOAD_NAME                1 (add)
             36 LOAD_CONST               3 (1)
             38 LOAD_CONST               5 (True)
             40 CALL_FUNCTION            2
             42 CALL_FUNCTION            1
             44 POP_TOP
 
  6          46 LOAD_NAME                2 (print)
             48 LOAD_NAME                1 (add)
             50 LOAD_CONST               3 (1)
             52 LOAD_CONST               6 (False)
             54 CALL_FUNCTION            2
             56 CALL_FUNCTION            1
             58 POP_TOP
             60 LOAD_CONST               7 (None)
             62 RETURN_VALUE

Po instrukcích bajtkódu následuje již zmíněná tabulka konstant. V prvním sloupci je číslo konstanty (použité u instrukce LOAD_CONST), ve druhém sloupci její hodnota (a nepřímo taktéž typ):

--------------------------------------------------------------------------------
0        ('a', 'b', 'return')
1        <code object add at 0x7f413d3d25b0, file "adder3.py", line 1>
2        add
3        1
4        2
5        True
6        False
7        None
--------------------------------------------------------------------------------
Python version: 3.8.10
Magic code: 550d0d0a
Timestamp: Thu Jun  8 17:13:13 2023
Size: 104
Hash: None
Bitfield: 0

Naopak ovšem vztah bool a int neplatí:

def add(a:bool, b:bool) -> bool:
    return a and b
 
print(add(1, 2))
print(add(1, True))
print(add(1, False))
print(add(True, False))

Mypy nyní nalezne hned několik chyb:

adder4.py:4: error: Argument 1 to "add" has incompatible type "int"; expected "bool"  [arg-type]
adder4.py:4: error: Argument 2 to "add" has incompatible type "int"; expected "bool"  [arg-type]
adder4.py:5: error: Argument 1 to "add" has incompatible type "int"; expected "bool"  [arg-type]
adder4.py:6: error: Argument 1 to "add" has incompatible type "int"; expected "bool"  [arg-type]
Found 4 errors in 1 file (checked 1 source file)

10. Datový typ seznam

Velmi důležitým datovým typem programovacího jazyka Python je seznam (list). U proměnných či parametrů typu seznam lze specifikovat nejenom to, že se jedná o seznam (to je neúplná typová informace), ale též typ hodnot prvků tohoto seznamu. V následujícím demonstračním příkladu je ukázána deklarace proměnné l, která je typu „seznam prvků typu int“. Současně je proměnná ihned inicializována tak, že obsahuje prázdný seznam (či spíše obsahuje referenci na prázdný seznam):

from typing import List
 
l: List[int] = []

Typová kontrola tohoto dvouřádkového příkladu dopadne následovně:

$ mypy list_type2.py
 
Success: no issues found in 1 source file

Samozřejmě nám nic nebrání v tom, aby se při inicializaci použily hodnoty jednotlivých prvků:

from typing import List
 
l: List[int] = [1, 2, 3]

Typová kontrola:

$ mypy list_type3.py
 
Success: no issues found in 1 source file

Samotný Python nám umožňuje jako prvky seznamu použít libovolné hodnoty (tudíž bude následující skript spustitelný), ovšem statická typová analýza odhalí, že hodnota None do seznamu nepatří:

from typing import List
 
l: List[int] = [1, 2, None]

Typová kontrola:

$ mypy list_type4.py 
 
list_type4.py:3: error: List item 2 has incompatible type "None"; expected "int"  [list-item]
Found 1 error in 1 file (checked 1 source file)

A opět se podívejme na to, jak je typovou analýzou řešen vztah mezi typy bool a int:

from typing import List
 
l: List[int] = [1, True, False]

Typová kontrola:

$ mypy list_type5.py
Success: no issues found in 1 source file

A naopak:

from typing import List
 
l: List[bool] = [True, False, 42]

Typová kontrola nyní nalezne, podle očekávání, problém:

$ mypy list_type6.py
 
list_type6.py:3: error: List item 2 has incompatible type "int"; expected "bool"  [list-item]
Found 1 error in 1 file (checked 1 source file)

11. Rozdíly mezi Pythonem 3.10 a předchozími verzemi

V Pythonu 3.10 došlo k vylepšení – u seznamů (ale i například u n-tic atd.) se již nemusí při uvádění typů používat speciální jména explicitně importovaná z modulu typing, tedy například List, Tuple atd. Namísto toho lze při specifikaci typu seznamu (včetně typu jeho prvků) použít list, což je jméno standardní třídy, kterou není zapotřebí nijak importovat. Celý zápis se tak zkrátí na pouhý jediný řádek:

l: list[int] = []

popř.:

l: list[int] = [1, 2, 3]

Pozor ovšem na to, že Mypy pro starší verze Pythonu nalezne v těchto příkladech chyby:

list_type1.py:1: error: "list" is not subscriptable, use "typing.List" instead  [misc]
Found 1 error in 1 file (checked 1 source file)

12. Datový typ n-tice

Zatímco u seznamů byla specifikace typu poměrně snadná, protože se pouze musel určit typ všech prvků seznamu, je tomu u n-tic (tuple) jinak, protože n-tice se (mj.) používají ve funkci záznamů (record). Proto je zde možné explicitně určit typ všech jejich prvků. Ostatně si to vyzkoušejme na několika příkladech.

from typing import Tuple
 
p: Tuple[int] = (1, 2, 3)

Tento zápis není korektní, protože specifikuje, že n-tice může obsahovat jediný prvek typu int a nikoli trojici prvků:

tuple_type1.py:3: error: Incompatible types in assignment (expression has type "Tuple[int, int, int]", variable has type "Tuple[int]")  [assignment]
Found 1 error in 1 file (checked 1 source file)

Pokud skutečně budeme chtít vytvořit n-tici se třemi prvky typu int (například pro reprezentaci barvy atd.), můžeme použít tento zápis:

from typing import Tuple
 
p: Tuple[int, int, int] = (1, 2, 3)

Nic nám však nebrání v tom určit, že každý prvek n-tice má být odlišného datového typu, což je ukázáno na dalším příkladu:

from typing import Tuple
 
p: Tuple[int, float, bool, str] = (1, 3.14, True, "Hello")

Výsledek statické typové kontroly:

Success: no issues found in 1 source file

A naopak si můžeme ukázat, kdy statická typová kontrola nalezne problematický kód:

from typing import Tuple
 
p: Tuple[int, float, bool, str] = (2.0, 3.14, 1, "Hello")
main.py:3: error: Incompatible types in assignment (expression has type "Tuple[float, float, int, str]", variable has type "Tuple[int, float, bool, str]")  [assignment]
Found 1 error in 1 file (checked 1 source file)
Poznámka: povšimněte si, že je přesně určeno, v čem chyba spočívá.

13. Zobrazení typových anotací funkcí

Typové anotace (hinty) funkcí a metod mohou být poměrně složité. Díky tomu, že anotace jsou funkcím přiřazovány po spuštění programu (viz naše prohlídka bajtkódu), je možné si tyto anotace nechat zobrazit v runtime, protože jsou dostupné jako atribut (funkce) se jménem __annotations__. Podívejme se nyní na několik příkladů.

Funkce bez specifikace typů:

def add(a, b):
    return a+b
 
 
print(add.__annotations__)

Anotace se vypíše jako prázdná mapa:

{}

Funkce se specifikací typů parametrů i typu návratové hodnoty:

def add(a:int, b:int) -> int:
    return a+b
 
 
print(add.__annotations__)

Nyní je anotace reprezentována mapou s klíči, které reprezentují názvy parametrů popř. název „speciálního parametru“ return (což je pochopitelně typ návratové hodnoty):

{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

Podívejme se ještě na složitější funkci, která dokáže spojit dva seznamy, z nichž každý obsahuje jako své prvky množiny:

from typing import List, Set
 
def add(a:List[Set[int]], b:List[Set[int]]) -> List[Set[int]]:
    return a+b
 
 
print(add.__annotations__)

Anotace získaná z atributu __annotations__ nyní bude vypadat následovně:

{'a': typing.List[typing.Set[int]], 'b': typing.List[typing.Set[int]], 'return': typing.List[typing.Set[int]]}

14. Kdy statická typová kontrola selže?

Mohlo by se zdát, že statická typová kontrola tak, jak je implementována v nástroji Mypy, dokáže nalézt všechny potenciální problémy související s typy. Ve skutečnosti tomu tak není a u jazyků typu Python ani být nemůže, protože proměnné lze modifikovat za běhu, a to kódem, o kterém statický analyzátor nemůže nic vědět. Ukažme si to na příkladech.

Následující zdrojový kód je plně staticky analyzovatelný (a korektní):

x = 21
 
 
def adder(a:int, b:int) -> int:
    return a+b
 
 
print(adder(x,x))

Výsledek statické kontroly:

$ mypy exec_problem_1.py
 
Success: no issues found in 1 source file

V případě, že do proměnné x vložíme hodnotu jiného typu, nástroj Mypy nás na tento problém upozorní:

x = "foo"
 
 
def adder(a:int, b:int) -> int:
    return a+b
 
 
print(adder(x,x))

Výsledek statické kontroly chybu odhalí:

$ mypy exec_problem_2.py
 
exec_problem_2.py:8: error: Argument 1 to "adder" has incompatible type "str"; expected "int"  [arg-type]
exec_problem_2.py:8: error: Argument 2 to "adder" has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Ovšem co se stane ve chvíli, kdy zavoláme exec, tedy funkci, která spustí příkaz zapsaný v řetězci (a tento řetězec lze získat různými způsoby)? Tuto skutečnost již statická analýza kódu nemůže odhalit:

x = 21
 
 
def adder(a:int, b:int) -> int:
    return a+b
 
 
exec("x='foo'")
 
print(adder(x,x))

Výsledek statické kontroly nyní bude vypadat následovně:

$ mypy exec_problem_3.py
 
Success: no issues found in 1 source file

Ovšem program po svém spuštění vypíše hodnotu, která jasně ukazuje, že byl použit řetězec:

$ python3 exec_problem_3.py
 
foofoo

15. Rozdíly v typových systémech: variance

„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 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 OOP má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ích kapitolách i v dalším článku. 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 (tomuto tématu se budeme věnovat v navazujícím článku).

16. Jak je tomu v Javě?

Java a TypeScript, na rozdíl od například jazyka Go, při volání funkcí a metod používají kovarianci, což může vést k běhovým (runtime) chybám. To si ostatně můžeme ukázat na velmi jednoduchých příkladech (subtyping je sice odlišný od hierarchie tříd, pro jednoduchost ovšem nyní budeme na chvíli tyto rozdíly ignorovat).

V prvním příkladu je použita snadno pochopitelná hierarchie tříd Ovoce, Hruska a Jablko. Ve statické metodě smichej v košíku mícháme hrušky s jablky. Ovšem této metodě předáváme košík představovaný polem hodnot typu Ovoce, takže vše bude v pořádku – jak v compile time, tak v runtime:

class Ovoce {
}
 
class Hruska extends Ovoce {
    public String toString() {
        return "Hruska";
    }
}
 
class Jablko extends Ovoce {
    public String toString() {
        return "Jablko";
    }
}
 
public class Variance1 {
    public static void smichej(Ovoce[] kosik) {
        kosik[0] = new Hruska();
        kosik[1] = new Jablko();
    }
 
    public static void main(String[] args) {
        Ovoce[] kosik = new Ovoce[2];
        smichej(kosik);
 
        for (Ovoce ovoce:kosik) {
            System.out.println(ovoce);
        }
    }
}

Až doposud bylo vše v pořádku. Ovšem co se stane ve chvíli, kdy metodě smichej předáme košík, do kterého je možné ukládat pouze hrušky (a to díky kovarianci můžeme)? Zde ono pověstné míchání hrušek s jablky nebude možné, ovšem překladač provádějící statickou typovou kontrolu tento problém neodhalí:

class Ovoce {
}
 
class Hruska extends Ovoce {
    public String toString() {
        return "Hruska";
    }
}
 
class Jablko extends Ovoce {
    public String toString() {
        return "Jablko";
    }
}
 
public class Variance2 {
    public static void smichej(Ovoce[] kosik) {
        kosik[0] = new Hruska();
        kosik[1] = new Jablko();
    }
 
    public static void main(String[] args) {
        Ovoce[] kosik = new Hruska[2];
        smichej(kosik);
 
        for (Ovoce ovoce:kosik) {
            System.out.println(ovoce);
        }
    }
}

Chyba je odhalena až za běhu, tedy v runtime:

Exception in thread "main" java.lang.ArrayStoreException: Jablko
        at Variance2.smichej(Variance2.java:19)
        at Variance2.main(Variance2.java:24)
Poznámka: bez čtení dalšího textu se pokuste zamyslet nad tím, jak (a zda vůbec) lze tento problém nějak uspokojivě vyřešit.

17. Chování Pythonu při práci se seznamy s typem

Podívejme se nyní na podobný příklad, ovšem tentokrát naprogramovaný v Pythonu s využitím typových anotací. Opět je vytvořena hierarchie tříd (a tedy i typů) Ovoce, Hruska a Jablko. Příklad dále obsahuje funkci smichej, která do košíku s ovocem přidá hrušku a jablko. Samotný košík je přitom představován typem List[Ovoce] a celý příklad je tedy bez problémů spustitelný a i Mypy v něm nenalezne žádný problém:

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

Výsledek testu, zda je kód typově korektní:

$ mypy Variance1.py
 
Success: no issues found in 1 source file

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):
        return "Hruska"
 
 
class Jablko(Ovoce):
    def __repr__(self):
        return "Jablko"
 
 
def smichej(kosik : List[Ovoce]):
    kosik.append(Hruska())
    kosik.append(Jablko())
 
 
kosik : List[Hruska] = []
 
smichej(kosik)
 
for ovoce in kosik:
    print(ovoce)

Tentokrát ovšem Mypy v kódy nalezne chyby a dokonce navrhuje způsob jejich opravy (tuto opravu ovšem nemůžeme použít):

$ mypy Variance2.py
 
Variance2.py:25: error: Argument 1 to "smichej" has incompatible type "List[Hruska]"; expected "List[Ovoce]"  [arg-type]
Variance2.py:25: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
Variance2.py:25: note: Consider using "Sequence" instead, which is covariant
Found 1 error in 1 file (checked 1 source file)

Zkusme si příklad zjednodušit, a to tak, že vytvoříme funkci 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 netuší, jaké operace se budou interně se seznamem provádět):

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

A Mypy skutečně stejnou chybu nalezne:

$ mypy Variance3.py 
 
Variance3.py:25: error: Argument 1 to "tiskni" has incompatible type "List[Hruska]"; expected "List[Ovoce]"  [arg-type]
Variance3.py:25: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
Variance3.py:25: note: Consider using "Sequence" instead, which is covariant
Found 1 error in 1 file (checked 1 source file)

18. Použití typu Sequence namísto typu List

Jak nástroj Mypy správně napovídá, 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í:

ict ve školství 24

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

Výsledek statické kontroly nástrojem Mypy:

$ mypy Variance4.py
 
Success: no issues found in 1 source file
Poznámka: znovu je nutné upozornit na to, že výsledek je korektní z toho důvodu, že se seznamem (nyní se sekvencí) pracujeme tak, jakoby se jednalo o neměnnou (immutable) datovou strukturu.

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

Všechny Pythonovské skripty i dva zdrojové kódy naprogramované v Javě, které jsme si v dnešním článku ukázali, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je nutné mít nainstalován balíček mypy, příklady naprogramované v Javě vyžadují celé JDK):

# Příklad Stručný popis Adresa
1 adder1.py funkce add bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder1.py
2 adder2.py funkce add s typovými anotacemi https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder2.py
3 adder3.py funkce add volaná s hodnotami TrueFalse https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder3.py
4 adder4.py funkce add akceptující hodnoty typu bool https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder4.py
5 adder5.py zobrazení typových informací pro funkci bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder5.py
6 adder6.py zobrazení typových informací pro funkci s typovými anotacemi https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder6.py
       
5 exec_problem1.py funkce add s typovými anotacemi https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/exec_pro­blem1.py
6 exec_problem2.py korektní detekce volání funkce add s nekompatibilními hodnotami https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/exec_pro­blem2.py
7 exec_problem3.py příkaz použitý v exec není statickým analyzátorem zachycen https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/exec_pro­blem3.py
       
8 list_type1.py typ seznam, s inicializací (bez prvků), pro Python 3.10 https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type1.py
9 list_type2.py typ seznam, s inicializací (bez prvků), pro starší verze Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type2.py
10 list_type3.py typ seznam, s inicializací (s prvky), pro starší verze Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type3.py
11 list_type4.py typ seznam, kontrola použití prvků s nekorektními typy https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type4.py
12 list_type5.py typ seznam, kontrola použití prvků s korektními typy https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type5.py
13 list_type6.py typ seznam, kontrola použití prvků s korektními typy https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type6.py
       
14 tuple_type1.py typ n-tice (nekorektní specifikace typu) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type1.py
15 tuple_type2.py typ n-tice (korektní specifikace typu) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type2.py
16 tuple_type3.py typ n-tice, v níž má každý prvek odlišný typ https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type3.py
17 tuple_type4.py typ n-tice, v níž má každý prvek odlišný typ https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type4.py
       
18 json_check.py delší kód v Pythonu bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/json_check.py
       
19 Variance1.java variance v Javě – korektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance1.java
20 Variance2.java variance v Javě – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance2.java
       
21 Variance1.py variance v Pythonu – korektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance1.py
22 Variance2.py variance v Pythonu – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance2.py
23 Variance3.py variance v Pythonu – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance3.py
24 Variance4.py použití typu Sequence namísto List https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance4.py
       
25 view_pyc.py jednoduchá prohlížečka souborů .pyc https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/view_pyc.py

20. Odkazy na Internetu

  1. mypy homepage
    https://www.mypy-lang.org/
  2. mypy documentation
    https://mypy.readthedocs.i­o/en/stable/
  3. Mypy na PyPi Optional static typing for Python
    https://pypi.org/project/mypy/
  4. 5 Reasons Why You Should Use Type Hints In Python
    https://www.youtube.com/wat­ch?v=dgBCEB2jVU0
  5. Python Typing – Type Hints & Annotations
    https://www.youtube.com/watch?v=QORvB-_mbZ0
  6. What Problems Can TypeScript Solve?
    https://www.typescriptlang.org/why-create-typescript
  7. How to find code that is missing type annotations?
    https://stackoverflow.com/qu­estions/59898490/how-to-find-code-that-is-missing-type-annotations
  8. Do type annotations in Python enforce static type checking?
    https://stackoverflow.com/qu­estions/54734029/do-type-annotations-in-python-enforce-static-type-checking
  9. Understanding type annotation in Python
    https://blog.logrocket.com/un­derstanding-type-annotation-python/
  10. Static type checking with Mypy — Perfect Python
    https://www.youtube.com/wat­ch?v=9gNnhNxra3E
  11. Static Type Checker for Python
    https://github.com/microsoft/pyright
  12. Differences Between Pyright and Mypy
    https://github.com/microsof­t/pyright/blob/main/docs/my­py-comparison.md
  13. 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
  14. Pyre: A performant type-checker for Python 3
    https://pyre-check.org/
  15. „Typing the Untyped: Soundness in Gradual Type Systems“ by Ben Weissmann
    https://www.youtube.com/wat­ch?v=uJHD2×yv7×o
  16. Covariance and contravariance (computer science)
    https://en.wikipedia.org/wi­ki/Covariance_and_contrava­riance_(computer_science)
  17. Functional Programming: Type Systems
    https://www.youtube.com/wat­ch?v=hy1wjkcIBCU
  18. A Type System From Scratch – Robert Widmann
    https://www.youtube.com/wat­ch?v=IbjoA5×VUq0
  19. „Type Systems – The Good, Bad and Ugly“ by Paul Snively and Amanda Laucher
    https://www.youtube.com/wat­ch?v=SWTWkYbcWU0
  20. Type Systems: Covariance, Contravariance, Bivariance, and Invariance explained
    https://medium.com/@thejameskyle/type-systems-covariance-contravariance-bivariance-and-invariance-explained-35f43d1110f8
  21. Statická vs. dynamická typová kontrola
    https://www.root.cz/clanky/staticka-dynamicka-typova-kontrola/
  22. Typový systém
    https://cs.wikipedia.org/wi­ki/Typov%C3%BD_syst%C3%A9m
  23. Comparison of programming languages by type system
    https://en.wikipedia.org/wi­ki/Comparison_of_programmin­g_languages_by_type_system
  24. Flow
    https://flow.org/
  25. TypeScript
    https://www.typescriptlang.org/
  26. Sorbet
    https://sorbet.org/
  27. Pyright
    https://github.com/microsoft/pyright
  28. Mypy: Type hints cheat sheet
    https://mypy.readthedocs.i­o/en/stable/cheat_sheet_py3­.html
  29. PEP 484 – Type Hints
    https://peps.python.org/pep-0484/
  30. Mypy online
    https://mypy-play.net/?mypy=latest&python=3.11

Autor článku

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