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 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.
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.
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:
What classes will the program include?
Broadly speaking, what will each class be responsible for, and what will it not be responsible for? For example, if you choose to use a model-view architecture, then each model class is responsible for updating an entity's state, however it should not know anything about how that entity will be displayed.
What attributes will each class have? What will be the types of those attributes?
What will be the important methods in each class? For each method, what will be its parameter types and return type?
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
solar.py: an animation of the inner solar system
asteroids.py: the beginning of an Asteroids game, including a ship that can move and fire bullets
platform.py: the beginning of a platformer game, including a character that can move and jump
You may wish to study these programs to see how they use classes and implement simple game physics.