Week 12: Notes

type checking

Some Python IDEs can perform type checking, which can report type errors in your program as you write it.

Specifically, Visual Studio Code's Pylance plugin includes Pyright, which is a type checker for Python. By default, type checking is disabled, but you can enable it by clicking "Type Checking" at the bottom of the window, then choosing a value in the menu that will appear at the top. There are four possible checking modes: "off" (the default), "basic", "standard" and "strict". (Alternatively, you can modify the setting "Python > Analysis: Type Checking Mode" on the Settings page.)

I generally recommend enabling at least basic type checking. It will report errors such as passing the wrong number of arguments to a function, or passing the wrong type of value to a library function. For example:

from math import sqrt

def sum(a, b, c):
    return a + b + c

y = sum(3, 4)  # error: argument missing for parameter 'c'

z = sqrt('hello')  # error: bad argument 

type hints

Type checking will not generally infer types for functions that you write. For example, consider this erroneous code:

from math import sqrt

def foo(x):
    return sqrt(x) + 2.5

z = foo('hello')

Clearly passing 'hello' to foo() makes no sense, but the standard type checker does not see that.

To improve the situation, we may specify a type hint (otherwise known as a type annotation) for the argument to foo():

def foo(x : float):
    return sqrt(x) + 2.5

The hint indicates that the argument must be a float. And now the type checker reports that the call foo('hello') is erroneous:

Argument of type "Literal['hello']" cannot be assigned to parameter "x" of type "float" in function "foo"
"Literal['hello']" is incompatible with "float"

A type hint may also indicate a function's return type:

def foo(x : float) -> float:   # foo() takes a float and returns a float
    return sqrt(x) + 2.5

Note that the CPython interpreter ignores type hints! For example, consider this function:

def id(x: int) -> int:
    return x

In the interpreter, we may pass it any kind of value, and no error is reported:

>>> id('hi')
'hi'
>>> id(True)
True

Nevertheless, you may want to provide type hints for some or all of your functions, for two reasons. First, as we have seen, they allow the IDE to find more bugs. Second, they are a useful form of documentation.

In fact you may use a type hint to specify the type of any variable. For example:

x: int = 4
y: str = 'yo'
z: list = [3, 'hi', True]
b: list[int] = [3, 4, 5]

Above, z is a list that can hold any type of value. b has type list[int], which is a more specific type, namely a list that can hold only integers. And so if you try to put another type of value in the list (e.g. b.append('red')) you'll get a type error. In type hints it's generally best to use the most specific type possible.

We may also specify types for tuples, sets and dictionaries:

t: tuple[str, int, bool] = ('hi', 77, True)
s: set[str] = { 'one', 'two', 'three' }
d: dict[int, str] = { 3 : 'red', 4 : 'blue', 5 : 'green' }

The type tuple[str, int, bool] is a triple whose elements have types str, int, and bool, respectively. The type dict[int, str] is a dictionary whose keys are ints and whose values are strings.

Now consider this function:

def abc(x: int | None):
    return 7 if x == None else x + 2

The type (int | None) is a union type. It indicates that x may be either an int, or None.

You may even assign names to your own types. For example:

Pos = tuple[float, float]
p: Pos = (3.0, 4.0)

Generally type hints are optional in Python. However, when type checking is enabled you may sometimes need to write type hints to get your programs to pass type checking. For example, consider this code:

# binary tree node class
class Node:     
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

n = Node(3)
n.left = Node(1)
n.right = Node(5)

The code is correct, however the last two lines above will not pass basic type checking:

Cannot assign member "left" for type "Node"
  Expression of type "Node" cannot be assigned to member "left" of class "Node"
    "Node" is incompatible with "None"
Cannot assign member "right" for type "Node"
  Expression of type "Node" cannot be assigned to member "right" of class "Node"
    "Node" is incompatible with "None"

The problem is that the type checker believes that the 'left' and 'right' attributes have type None, so it won't allow a Node to be assigned to them. To fix this, we can add type hints to the initializer method:

    def __init__(self, val):
        self.val = val
        self.left : Node | None = None
        self.right : Node | None = None

Now the code passes with no type errors.

Python also supports various other kinds of types (e.g. generic types) that are beyond the scope of this course. However, the set of types that we've covered should be sufficient for you to write many type hints.

dataclasses

Consider the following class, intended to store data about a city:

class City:
    def __init__(self, name, lat, long, population):
        self.name = name
        self.lat = lat
        self.long = long
        self.population = population

The initializer method is a bother to write. Another inconvenience is that a City's string representation doesn't show any of its attributes:

>>> c = City('Prague', 50.1, 14.4, 1_300_000)
>>> c
<__main__.City object at 0x7f02e2b61d50>

We could write a __repr__ method to fix that, but that would be a further chore.

As an alternative, Python supports dataclasses, which are an easier way to write classes intended to hold data. We can write the City class as a dataclass:

from dataclasses import dataclass

@dataclass
class City:
    name: str
    lat: float
    long: float
    population: int

In a dataclass definition, we simply write each attribute's name and its type. This syntax is easy and convenient. Python automatically generates an initializer method, as well as a __repr__ method so that instances will print nicely:

>>> c = City('Prague', 50.1, 14.4, 1_300_000)
>>> c
City(name='Prague', lat=50.1, long=14.4, population=1300000)

The Python interpreter (CPython) ignores the types in a dataclass definition, just like other type hints. However, the type checker will see and enforce these types. For example, the call City(1, 2.0, 3.0, 4) will produce a type error, since 1 is not a string.

You may also specify default values in a dataclass definition. For example:

@dataclass
class City:
    name: str
    lat: float
    long: float
    population: int = 0

Now if we create a City and don't specify its population, it will automatically be set to zero.

Dataclasses have various other features, which you can read about in the Python documentation. But the syntax I've presented here should be enough for writing many dataclasses.

object-oriented design

Before you write any larger program, you'll want to think about its design. In an object-oriented design, we use classes and objects to represent most entities that we are modeling. This is often a good way to structure a large, complex program.

Program design is an art as much as a science, and I can't give you precise rules to use in designing a program. However, here are some questions that you should probably ponder before writing any code at all:

pygame

Last week we discussed Python's Tkinter library for writing graphical user interface. Another popular graphics library is pygame. If you want to write a game or other program with real-time graphics, pygame may be a better choice than Tkinter.

For learning pygame I recommend two books by Al Sweigart:

You can read both of these books online for free. For that reason, I won't give a detailed Pygame introduction here. However, I have made a pygame quick reference page that lists many useful classes and methods. You may also wish to refer to the official Pygame web site and documentation, though it is not always so easy to find your way around it.

In the lecture we looked at a few programs written with pygame including

You may wish to study these programs to see how they use classes and implement simple game physics.