Last active
October 2, 2019 05:08
-
-
Save mpkocher/e123aa0613242715717d291f9df7afdd to your computer and use it in GitHub Desktop.
Exploring TypedDict in Python 3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Output from mypy 0.711 | |
typeddict_pad.py:28: error: Module 'typing' has no attribute 'TypedDict'; maybe "_TypedDict"? | |
typeddict_pad.py:28: error: Name 'TypedDict' already defined (possibly by an import) | |
typeddict_pad.py:90: error: Unsupported operand types for + ("str" and "int") | |
typeddict_pad.py:107: error: Argument 2 has incompatible type "int"; expected "str" | |
typeddict_pad.py:110: error: Unsupported operand types for + ("int" and "str") | |
typeddict_pad.py:118: error: "Movie" has no attribute "clear" | |
typeddict_pad.py:128: error: Argument 1 to "update" of "TypedDict" has incompatible type "Dict[str, object]"; expected "TypedDict({'name'?: str, 'year'?: int})" | |
typeddict_pad.py:137: error: Argument 1 to "update" of "TypedDict" has incompatible type "Movie"; expected "TypedDict({'name'?: str, 'year'?: int})" | |
typeddict_pad.py:167: error: Argument 2 has incompatible type "int"; expected "str" | |
typeddict_pad.py:173: error: "Movie2" has no attribute "clear" | |
typeddict_pad.py:189: error: Unsupported operand types for + ("None" and "int") | |
typeddict_pad.py:189: note: Left operand is of type "Optional[int]" | |
typeddict_pad.py:221: error: Cannot use isinstance() with a TypedDict type | |
""" | |
import logging | |
import sys | |
from typing import Optional | |
try: | |
# Python < 3.8 | |
# pip install mypy_extensions | |
from mypy_extensions import TypedDict | |
except ImportError: | |
# Python 3.8 | |
from typing import TypedDict | |
log = logging.getLogger() | |
FUNCS = [] | |
def register(f): | |
FUNCS.append(f) | |
def fx(*args, **kwargs): | |
return f(*args, **kwargs) | |
return fx | |
def log_expected_error(ex, fx): | |
raised_error = False | |
try: | |
return fx() | |
except ex as e: | |
raised_error = True | |
log.info(f"Got Expected error `{e}` of type {ex} from {fx.__name__}") | |
finally: | |
if not raised_error: | |
log.error(f"Expected {fx} to raise {ex}") | |
class Movie(TypedDict): | |
name: str | |
year: int | |
@register | |
def example_00() -> int: | |
"""Simple Example of Using raw dict and how mypy won't catch | |
these errors with the keys | |
""" | |
m = dict(name='Star Wars', year=1234) | |
def f(): | |
# mypy will NOT catch this error | |
t = m['name'] + 100 | |
log_expected_error(TypeError, f) | |
return 0 | |
@register | |
def example_01() -> int: | |
"""Use TypedDict to see what static type check errors are | |
found by mypy""" | |
m = Movie(name='Star Wars', year=1977) | |
log.info(m) | |
def f() -> int: | |
# the type checker will catch this | |
n = m['name'] + 100 | |
return n | |
log_expected_error(TypeError, f) | |
return 0 | |
@register | |
def e_02() -> int: | |
"""Now explore starting to mutate the Movie | |
and mixing Movie instances with raw dicts | |
""" | |
m = Movie(name='Star Wars', year=1977) | |
log.info(m) | |
# mypy will catch this | |
m['name'] = 11111 | |
def f() -> int: | |
m['year'] = m['year'] + 'asfdsasdf' | |
return 0 | |
log_expected_error(TypeError, f) | |
# Use dict methods to mutate | |
# mypy is confused by this and generates | |
# `"Movie" has no attribute "clear"` | |
m.clear() | |
def f2() -> int: | |
return m['year'] + 100 | |
log_expected_error(KeyError, f2) | |
d2 = dict(extras=True, alpha=1234, name=12345, year='1978') | |
# mypy will raise TypeError here | |
m.update(d2) | |
log.info(m) | |
# Update a Movie with a Movie | |
m2 = Movie(name='Star Wars', year=1977) | |
new_m = Movie(name='Movie Title', year=1234) | |
# No mypy type error | |
m.update(new_m) | |
log.info(m2) | |
return 0 | |
# I find this entire idea of total=False | |
# to be confusing from a type | |
# standpoint. This means that every field of type T | |
# is fundamentally Optional[T]. How is this | |
# helping to communicate the interface and not | |
# raise a not of NPEs? | |
class Movie2(TypedDict, total=False): | |
name:str | |
year:int | |
release_year: Optional[int] | |
@register | |
def e_03() -> int: | |
""" | |
Explore with defining an 'incomplete' Movie data model and how | |
None/Null checking works with mypy | |
""" | |
m = Movie2(name='Star Wars') | |
log.info(m) | |
def f() -> int: | |
# mypy will catch this | |
m['name'] = 1234 | |
return 0 | |
# Use dict methods to mutate | |
# mypy is confused by this. The error is: | |
# `"Movie" has no attribute "clear"` | |
m.clear() | |
def f2() -> int: | |
# mypy doesn't catch this NPE | |
# I don't think it treats the type | |
# as Optional[int] | |
m['year'] = m['year'] + 100 | |
return 0 | |
log_expected_error(KeyError, f2) | |
# Explicit test with release_year which | |
# is fundamentally Optional[int] | |
def f3() -> int: | |
# mypy WILL catch this NPE | |
m['release_year'] = m['release_year'] + 100 | |
return 0 | |
log_expected_error(KeyError, f3) | |
# This works as expected and | |
m2 = Movie2(name='Star Wars', release_year=2049) | |
# This works as expected and mypy won't raise an error | |
if m2['release_year'] is not None: | |
_ = m2['release_year'] + 10 | |
return 0 | |
@register | |
def e_04() -> int: | |
"""Testing isinstance""" | |
m = Movie(name='Movie', year=1234) | |
def f() -> int: | |
is_true = isinstance(m, dict) | |
return 0 | |
# This is a bit unexpected that this | |
# will raise an exception at runtime | |
# ` Cannot use isinstance() with a TypedDict type` | |
def f2() -> int: | |
is_true = isinstance(m, Movie) | |
return 0 | |
log_expected_error(TypeError, f2) | |
return 0 | |
def run_main() -> int: | |
"""Note, each func needs a type, otherwise mypy will skip static analysis. Hence | |
all the example funcs return an int | |
""" | |
formatter = '%(asctime)s [%(levelname)s] %(lineno)s %(msg)s' | |
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=formatter) | |
# this will raise from mypy | |
#nx: Optional[int] = None | |
#log.info(nx) | |
#t = nx + 10 | |
def runner(f) -> int: | |
log.info(f"Running {f.__name__}") | |
return f() | |
_ = list(map(runner, FUNCS)) | |
log.info("Exiting main with exit code 0") | |
return 0 | |
if __name__ == '__main__': | |
sys.exit(run_main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment