Skip to content

Commit

Permalink
Adding type annotations demo files
Browse files Browse the repository at this point in the history
I'm adding the four files I used for my demo of type annotations & mypy static type checking.
  • Loading branch information
Graham Kanarek authored Jan 3, 2018
1 parent 6591f4b commit 81102bb
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 0 deletions.
78 changes: 78 additions & 0 deletions typing_demo/typing_demo_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
mypy + typing demo for JWQL dev meeting 2018-1-3
Part 1: Intro
"""

import sys
from typing import (List, Set, Dict, Tuple, Union, Optional, Callable,
Iterable, Any)

assert sys.version_info >= (3, 6) # PEP 526 added variable annotations

an_integer: int = 1
a_float: float = 1.0
a_bool: bool = True
a_string: str = "jwql"
a_list: List[int] = [1]
a_set: Set[int] = {1, 2, 3}
a_dict: Dict[str, bool] = {'jwql':True} # Have to specify both keys and values

#For python versions prior to 3.6, the variable annotation syntax uses comments:
# annotated_variable = 1 # type: int

#Tuples are a little different - we can specify a type for each element of a
#tuple because they're immutable
a_heterogeneous_tuple: Tuple[int, bool] = (1, True)
an_empty_tuple: Tuple[()] = ()

#For heterogeneous non-tuples, use Union.
a_heterogeneous_list: List[Union[int, bool, str]] = [1, True, "jwql"]
a_heterogeneous_dict: Dict[Union[str, int], Union[bool, int]] = {"jwql": True, 1:1}

#If a value can be None, use Optional
maybe_a_string: Optional[str] = "jwql" if not a_bool else None

#For functions, there's a similar annotation syntax
def a_generic_function(num: int) -> str:
return f"You passed {num} to this completely generic function."

def two_arg_function(name: str, num: float = 0.0) -> None:
print(f"Sorry {name}, this function won't return {num}")

#Function aliases and anonymous functions can also be annotated with the
#same variable syntax

func_alias: Callable[[str, float], None] = two_arg_function
anon_func: Callable[[Any], int] = lambda x: 1

#Generators are just functions which return iterables:
def a_generator() -> Iterable[int]:
i = 0
while True:
yield i
i += 1

#NOT RECOMMENDED
my_metavar: "hey i'm metadata!" = "not metadata"
print(__annotations__["my_metavar"])

#Type annotations are stored in __annotations__, either as a local variable
#or as an object attribute.

def print_annotations(arg: Any) -> bool:
if not hasattr(arg, "__annotations__"):
print("Sorry, that argument doesn't have its own __annotations__.")
return False
print(arg.__annotations__)
return bool(arg.__annotations__)

for name in ["an_integer", "a_generic_function", "two_arg_function",
"func_alias", "anon_func", "a_generator"]:
var = locals()[name]
print(f"Annotations for {name}:")
if not print_annotations(var):
print("Instead, we'll check the local instance of __annotations__:")
print(__annotations__[name])
83 changes: 83 additions & 0 deletions typing_demo/typing_demo_2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
mypy + typing demo for JWQL dev meeting 2018-1-3
Part 2: More advanced techniques
"""

from typing import (Iterable, Sequence, Mapping, MutableMapping, Any, List,
Tuple, IO, ClassVar, NewType, Set, Union)
from astropy.io import fits
import numpy as np

#Use the above generic types for standard duck typing of functions, in the
#same way that you'd use abstract base classes

def needs_an_iterable(iterable_arg: Iterable[Any] = []) -> List[str]:
return [str(x) for x in iterable_arg]

def dont_mutate(immut_dict: Mapping[Any, Any]) -> List[Tuple[Any, Any]]:
return list(immut_dict.items())

def do_mutate(mut_dict: MutableMapping[Any, Any]) -> Set[Any]:
mut_dict['jwql'] = True
return set(mut_dict.keys())

#Variables can be annotated without initializing
stream: IO[str]
print(__annotations__['stream'])

#The IO type doesn't distinguish between reading, writing, or appending.
with open('demo.txt', 'w') as stream:
for i in range(10):
stream.write(f"{i}\n")

#Pre-annotation is also useful with conditional branches
conditional: str
if "jwql":
conditional = "Yay!"
else:
conditional = "Boo!"

#Data types from imported modules can be used just as easily as builtin types
an_array: np.ndarray = np.arange(10)
a_fits_header: fits.Header = fits.getheader("nirspec_irs2_nrs1_i_02.01.fits")

#Class attributes and methods can be annotated as well, and user-defined
#classes can be used to annotate other variables and functions.
class aClass(object):
x: int = 0 #this is an instance variable with a default value
y: ClassVar[List[int]] #this is a class variable with no default value

def __init__(self) -> None: #doesn't return anything
self.x = 1
self.y = [2]
#Can also annotate attributes in __init__
self.z: np.float64 = np.float64(3.0)
print(__annotations__)

def result(self) -> np.float64: #self shouldn't be annotated
return x + np.array(self.y).sum() + self.z

print(aClass.__annotations__)
an_instance: aClass = aClass()
print(__annotations__["an_instance"])
print(an_instance.__annotations__)

#You can use forward references if you like defining things out of order
def preemptive_function(num: "Numberlike", user: "UserID") -> None:
#Note that neither Numberlike or UserID have been defined.
print(f"Y'know, {user}...")
print(f"{num} should probably be some kind of number.")
print("Just saying...")

#You can also define new types and type aliases
Numberlike = Union[int, float, np.float64]
UserID = NewType('UserID', str)

#Note that you can do anything with UserID that you can do with a string,
#and can pass a UserID to any function that would accept a string. However,
#operations on UserIDs will always result in strings, not UserIDs.
output = UserID('Gray') + UserID('Kanarek')
print(output) # is of type string, not UserID.
57 changes: 57 additions & 0 deletions typing_demo/typing_demo_3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
mypy + typing demo for JWQL dev meeting 2018-1-3
Part 3: mypy for type checking
Many thanks to Tommy Tutone...
"""

from typing import NewType

#mypy can check for incorrectly-typed variables

bad_variable: str = 1 #no runtime error

#This can especially be useful when using pre-annotation, since types can be
#hinted before calculations, I/O, or other complex code determines its value.
jenny: str

#mypy can also check function arguments and return values
def ive_got_your_number(num: int) -> bool:
if num == 867_5309:
return True
else:
return "Jenny don't change your number"

ive_got_your_number("jenny") #no runtime error
ive_got_your_number(555_1212) #no runtime error

if ive_got_your_number(8675_309):
jenny = 867_5309 #no runtime error
else:
jenny = "Don't change your number"

#If for some reason you don't want a particular variable's type to be checked,
#then use comment syntax and "ignore"
dummy = None # type: ignore # otherwise this will throw a mypy error!

#mypy can handle user-created types
UserID = NewType("UserID", str)

gray: UserID = UserID("Gray")
kanarek: UserID = "Kanarek" #no runtime error

user: UserID = gray + kanarek #no runtime error

def get_first_char(user: UserID) -> str:
return user[0]

get_first_char(gray)
get_first_char("Gray") #no runtime error


#mypy can help you figure out the types of variables, if it's complicated to
#find out beforehand
reveal_type(1)
17 changes: 17 additions & 0 deletions typing_demo/typing_demo_4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
mypy + typing demo for JWQL dev meeting 2018-1-3
Part 4: Subtlety
"""

#Why do we care about this? Because errors can be subtle.

#A simple example!

def get_favorite_number():
return input("What's your favorite number? ")

num = get_favorite_number()
print("Twice your favorite number is", num*2)

0 comments on commit 81102bb

Please sign in to comment.