As I turn 24, my blog is about to turn 1 year old!! 🎊
This article will mostly be a summary of the official python documentation. I have found it useful to read a documentation and then re-summarize it in my own words. (Examples are mostly theirs though)
Typing is a new module starting python 3.5 and introduced by PEP 484. Its goal is to introduce static types to Python.
Table of Content
- To Clarify One Thing
- Hello World Example
- Custom Types with NewType
- Callable Type
- Generic Types
- Union Types
- If You Must Remember 3 Things
To Clarify One Thing
To clarify something from the start:
Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.
This is taken from the PEP’s Non Goal section.
So even if type hints are available, they are not checked at runtime (even though there is a support for that through
get_type_hints()). Instead, type hints are meant to be checked by offline type checkers or linters.
Hello World Example
Here is how type hinting might work:
def hello_human(name: str) -> str: return 'Hello ' + name
without type hints, the function would look like this:
def hello_human(name): return "Hello " + name
You might be saying to yourself that it isn’t much difference… But after a year of experience with Scala I can confirm that types can really make a code more readable.
Custom Types with NewType
If the built-in types are not enough, NewType allows us to create custom types:
from typing import NewType Url = NewType("Url", str) website = Url("fares.codes")
One thing to pay attention for
Operations on newtypes will be treated as operations on the underlying type and the result is the underlying type.
Here’s an example:
UserId = NewType("UserId", int) fares = UserId(1) emily = UserId(3) total = fares + emily ## Will output 4
So what is the point? It allows its users to pass any int when a UserId is expected, but at the same time doesn’t allow them to create a UserId in an invalid way.
At runtime, the NewType expression is transformed to a regular function call and therefore no additional overhead is introduced.
Warning: Since NewTypes do not exist at runtime, it is impossible to create subtypes of NewTypes.
UserId = NewType('UserId', int) # Fails at runtime and does not typecheck class AdminUserId(UserId): pass
However, it is possible to create a NewType based on another NewType:
UserId = NewType('UserId', int) ProUserId = NewType('ProUserId', UserId)
Return type can also be a
Callable. It follows this signature:
Callable[[Arg1Type, Arg2Type], ReturnType]
from typing import Callable def feeder(get_next_item: Callable[, str]) -> None: # Body def async_query(on_success: Callable[[int], None], on_error: Callable[[int, Exception], None]) -> None: # Body
If we don’t want to declare the types of the arguments, we can use the following syntax:
A normal example of a sequence of UserIds would look like this:
UserId = NewType('UserId', int) def do_something(sequence: Sequence[UserId]) -> None: ...
But what if we wanted the input to the function to be generic?
from typing import Sequence, TypeVar T = TypeVar('T') def do_something(sequence: Sequence[T]) -> None: ...
Classes in python can also be generic:
from typing import TypeVar, Generic T = TypeVar('T') class MyClass(Generic[T]): def __init__(self, value: T, name: str) -> None: self.name = name self.value = value
Type variables can also be constrained to specific types:
S = TypeVar('S', int, str)
In that case, the type S can either be a str or an int.
Note: by default type variables are invariant, but they can be marked as covariant or contravariant by passing:
contravariant=True. Additionally, an upper bound can be specified using:
What is the difference between the following two declarations:
UserId = NewType('UserId', int)
from typing import Type UserId = NewType('UserId', Type[int])
T_int = int UserId = NewType('UserId', T_int)
T_int = int and
Type[int] are the same thing. They allow the variable to accept a type int but to also accept
class object of int.
While the first example, only allows the variable to be of type int.
Example from the doc:
class User: ... class BasicUser(User): ... class ProUser(User): ... class TeamUser(User): ... # Accepts User, BasicUser, ProUser, TeamUser, ... def make_new_user(user_class: Type[User]) -> User: # ... return user_class()
When using types, we can only pass classes, Any, type variables or a Union of those.
Union types are used to indicate an one of multiple types. It is used as
Union[X, Y]. Example:
Union[int, str, float] Union[str, int]
Optional allows us to define a type that can also be None. Example:
def foo(arg: Optional[int] = None) -> None: ...
Optional[X] is also equivalent to:
If you must remember 3 things
- Type hints make your code more readable so you should try to use them as often as you can
- Type hints are only checked by static type checkers or linters and not during runtime
- I just turned 24 😄