-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
4 changed files
with
235 additions
and
0 deletions.
There are no files selected for viewing
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
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]) |
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
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. |
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
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) |
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
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) |