Spokane Python User Group

Typing for Fun and Profit

Joseph Riddle


  • Static or dynamic
  • Strong or weak
  • But what is duck typing?
  • typing in Python

This presentation was built on the shoulders of others.



Static or Dynamic?

Static: type cannot change at runtime

types are associated with a variable declaration

Dynamic: type can determined at runtime

It's also very dynamic as it rarely uses what it knows to limit variable usage. In Python, it's the program's responsibility to use built-in functions like isinstance() and issubclass() to test variable types and correct usage.

https://wiki.python.org/moin/Why is Python a dynamic language and also a strongly typed language

Static or Dynamic -- Languages

  • Dynamic
    • Python*
    • Ruby
    • Clojure
    • JavaScript
  • Static
    • C/C++
    • Rust
    • Java
    • TypeScript

Static or Dynamic -- Examples


>>> thing = 42
>>> type(thing)
<class 'int'>
>>> thing = 'hello'  # wow, so dynamic 😮
>>> type(thing)
<class 'str'>


var thing = 42;             // Java 10 introduced var
thing = "hello";            // I've got a bad feeling about this...
Main.java:5: error: incompatible types: String cannot be converted to int
    thing = "hello";

Strong or Weak?


restrictive about how types can be intermingled


in a weakly typed language a compiler / interpreter will sometimes change the type of a variable

https://wiki.python.org/moin/Why is Python a dynamic language and also a strongly typed language

Strong or Weak -- Languages

  • Strong
    • Python
    • Ruby
    • Java
    • Clojure
  • Weak
    • C/C++
    • JavaScript


Strong or Weak -- Examples

Python 💪

>>> foo = 42
>>> bar = 'no thank you'
>>> foo + bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'


foo = 42
bar = 'yes please'
foo + bar
> 42yes please  // booo 👎

A very well drawn illustration


Python is strongly typed because it restricts how types interact and dynamically typed becuase types are determined and can change at runtime.

Duck Typing 🦆

A programming style which does not look at an object’s type to determine if it has the right interface; instead, the method or attribute is simply called or used (“If it looks like a duck and quacks like a duck, it must be a duck.”)


Nominal vs. Structual subtyping

Nominal: If Foo inherits from Var, we've named our supertype Bar.

Structural: Foo is a subtype of Bar because its structure matches Bar.

Duck typing ~ Structural typing

Pause for questions 🎙


Support for type hints

Thank you for coming 👋

Just kidding.

A brief type hint timeline

Tour de typing 🚲


You're technically using type hints

A static type checker will treat every type as being compatible with Any and Any as being compatible with every type.

Examples: see examples/any.py


typing.Any -- cont'd

from typing import Any

thing: Any = 1
thing = 2                       # this is fine
thing = 'hello'                 # this is fine, too
thing = { 'foo': 'bar' }        # totally fine with this
thing = lambda foo: foo.bar()   # super fine
def some_function(data):
    return data

def some_typed_function(data: Any) -> Any:
    return data


StringOrNone = Union[str, None]

string: StringOrNone = 'foo'
none: StringOrNone = None
not_string_or_none: StringOrNone = 42  # <- error
Number = Union[int, float]
def multiply(left: Number, right: Number) -> Number:
    return left * right

multiply(5, 10.0)


def call_this_funky_func(func):
    return func()

call_this_funky_func(5)  # <- TypeError: 'int' object is not callable
from typing import Callable

def call_this_typed_funky_func(func: Callable[..., int]):
    return func()

call_this_typed_funky_func(5)  # <- Type "Literal[5]" cannot be assigned to type "(*args: Any, **kwargs: Any) -> int"
call_this_typed_funky_func(lambda: 5)
def call_this_other_typed_funky_func(func: Callable[[int, int], int]):
    # do some more interesting stuff...
    x = 5
    y = 10
    return func(x, y)

call_this_other_typed_funky_func(lambda x, y: x ** y)


from typing import Literal

JoeOrJoseph = Literal['joe', 'joseph']

def only_joe_or_joseph(value: JoeOrJoseph):
    return f'Hi {value}!'

only_joe_or_joseph('not joseph')  # <- error


Type variables exist primarily for the benefit of static type checkers. They serve as the parameters for generic types as well as for generic function definitions.

from typing import TypeVar

T = TypeVar('T')
A = TypeVar('A', str, bytes)  # Must be str or bytes


from typing import Generic, TypeVar

T = TypeVar('T')

class CacheService(Generic[T]):

    def __init__(self) -> None:
        self.cache = {}

    def get(self, key) -> T:
        return self.cache[key]

    def set(self, key, value: T) -> None:
        self.cache[key] = value

typing.Generic -- cont'd

any_cache_service = CacheService()
any_cache_service.set('foo', 'bar')
foo = any_cache_service.get('foo')  # can't tell what foo is

typed_cache_service = CacheService[str]()
typed_cache_service.set('foo', 'bar')
foo = typed_cache_service.get('foo')  # foo is a str


I need my duck typing!

Similar idea to mixins.

from typing import Protocol

# some other module
class Duck():
    def speak(self):
        return 'Quack!'

# our module
class Speaks(Protocol):
    def speak(self) -> str: ...

def speak_louder(speaker: Speaks) -> str:
    return speaker.speak().upper() + '!'

speak_louder(Duck()) # <-- notice how Duck does not nominally inherit from Speaks


from typing import Optional

def square(item: Optional[int]):
    if item is None:
        return None
    return item ** 2

result = square(2)  # int | None

Bummer, I don't want to do a None check for every result...


from typing import Optional, overload

def square(item: None) -> None: ...

def square(item: int) -> int: ...

def square(item: Optional[int]):
    if item is None:
        return None
    return item ** 2

result = square(2)     # int
result = square(None)  # None


from typing import cast

my_config = get_config_var('my_config')
reveal_type(my_config)  # Any -- note: reveal_type is from mypy

my_config = cast(
    Dict[str, int],
reveal_type(my_config)  # Dict[str, int]

Code sample used from Type-checked Python in the real world


This module defines several types that are subclasses of pre-existing standard library classes which also extend Generic to support type variables inside []. These types became redundant in Python 3.9 when the corresponding pre-existing classes were enhanced to support [].

The redundant types are deprecated as of Python 3.9 but no deprecation warnings will be issued by the interpreter. It is expected that type checkers will flag the deprecated types when the checked program targets Python 3.9 or newer.

The deprecated types will be removed from the typing module in the first Python version released 5 years after the release of Python 3.9.0. See details in PEP 585—Type Hinting Generics In Standard Collections.



Note - cont'd

< 3.9

from typing import Tuple

str_and_int: Tuple[str, int] = ('foo', 42)

>= 3.9

str_and_int: tuple[str, int] = ('foo', 42)


Structural Pattern Matching



Thank you for coming!