ICS 32A Fall 2023
Notes and Examples: Type Annotations


Statically- and dynamically-typed languages

Depending on what programming languages you've learned in the past, you might feel right at home in Python, but you might also feel a bit like a fish out of water. There are a number of ways that different programming languages differ from one another. One of the key distinctions between programming languages is in their handling of types.

You can think of all programming languages as existing on a spectrum, with respect to how and when they handle types, with the two ends of that spectrum being these.

Note that the reason I say there is a spectrum here is because there exists middle ground — languages like C++ and Java, for example, mostly check types before execution, but also allow some of those checks (in the form of casts) to be deferred until the program runs. The usual tradeoff if you want types to be checked statically (i.e., before the program runs) is that you'll need to say something explicitly about types in your program; Java and C++, for example, require a type to be declared for every variable, and then will verify that what you do with those variables matches your stated intent.

As we've seen already, Python is firmly on the dynamic end of the spectrum. For the most part, if you follow Python's structural rules — the syntax of the language — you'll have a Python script that can be executed, though that script may fail during execution. It's not necessary to specify the types of variables, functions' parameters or results, and so on, because there's no explicit need; the types won't be checked until the program is executing, by which time those types will be clear (e.g., a variable's "type" at any given time would simply be the type of object currently stored in that variable).

However, that's not the end of the story of types in Python. When we write programs, we don't just write them for computers to execute; we write them for people to read and understand, as well. So, to the extent that we can communicate a fuller understanding of the intent of the code we write without negatively affecting how the program runs, everyone is better off. Python offers that ability: Even though types aren't checked until run time, we can nonetheless describe them in a fairly unambiguous way using Python syntax.


Type annotations

Recent versions of Python have added a syntax for type annotations, which allow you to specify information about the types that will be processed by the code you're writing. Type annotations are more open-ended than you might imagine at first. In fact, they mainly serve as a way to specify this information for people; when programs run, the type annotations are, for the most part, ignored. (There is additionally a longer-term goal of introducing tools that can be used to check types in a Python program before running it, though this is still a developing area of the Python community and lies beyond the scope of our work this quarter.)

A type annotation can actually be just about any arbitrary Python expression you can write. There are only two requirements that you need to meet:

  1. The expression must be syntactically legal, which is to say that, structurally, it has to be Python.
  2. If you use an identifier (i.e., the name of something, such as a type or a function), it exists already, and you're not using it in a way that's incompatible with what it is (e.g., you can't follow it with a slice operation unless it's an object that supports one).

Of course, the true goal is to communicate type information in a way that people can read and understand, so we'll try to write our type annotations in a way that best achieves that. To the extent that you can name the types explicitly, your annotations will be more readable; as we'll see, though, there are times when we'll need to use more open-ended techniques.

Annotating functions

When you write a function, you can add annotations to its parameters and its return value. It's not necessary to add annotations to all parameters — you can pick and choose — and it's also not necessary to annotate the return type. In general, though, you'll want to annotate anything you can.

A type annotation is written on a parameter by following its name with a colon and then an expression describing its type. The return type of a function is described by following the parameter list with an arrow — technically, a dash followed by a greater-than sign, i.e., -> — after which you would write an expression describing its type.

As an example, suppose we were writing a function len_at_least that takes two parameters, a string and an integer, and returns True if the length of the string is at least the given integer value. So, for example, len_at_least('Boo', 2) would return True (because the length of 'Boo' is 3, which is greater than 2), while len_at_least('Alex', 10) would return False (because the length of 'Alex' is 4, which is less than 10). We might write the function, complete with type annotations and a docstring, this way:


def len_at_least(s: str, min_length: int) -> bool:
    'Returns True if the length of s is at least the given minimum'
    return len(s) >= min_length

When you write functions in this course, you're going to want to include both the type annotations and the docstring. If you can't succinctly describe what the purpose of your own function is — what it does, what its inputs are, what its output is, how it might fail — you're probably not ready to write it. To solidify your thinking, we'll require the type annotations and docstrings.

Annotating variables

It is also possible to annotate the types of variables, as well. There are a couple of ways to do it. One is to introduce the type annotation separately from giving it a value.


age: int
age = 45

Perhaps surprisingly, note that the variable age doesn't yet have a value until after it's first assigned, even if a type annotation precedes that assignment; however, it will be created in the scope where you annotated it, even if it's never assigned. Otherwise, the annotation doesn't actually do anything, other than communicate your intent to a human reader of your program.

You can also combine the type annotation into an assignment statement, if you prefer, which is shorter to write, if perhaps a little bit confusing when names get longer.


age: int = 45

We are not requiring you to annotate every variable in this course. Variables should generally have a limited scope; generally, they'll be local to a single function. Since your functions will be short and simple — with each one having a single responsibility — there's less value in variable annotations, from a readability perspective. However, you should at least be considering what types of values you intend to store in your variables, and if type annotations on variables help you to clarify that, you should certainly feel free to use them.

What annotations are allowed to say

There is technically no restriction on what you can say in a type annotation; any legal Python expression will do. However, a rule of thumb for us is to use these rules.

Putting these ideas together, here is a function that takes a list of integers or floats and computes their sum, complete with type annotations and a docstring.


def sum_numbers(nums: list[int | float]) -> int | float:
    'Computes the sum of the numbers in the given list'

    total = 0

    for num in nums:
        total += num

    return total


Remember: Annotations are not checked

A really important thing to keep in mind, especially if you've written programs in languages that have static type checking, is that the type annotations you'll write in Python will not be checked. You'll be able to write annotations that are incomplete, or even completely nonsensical, and the program may still run just fine. Still, you'll want to be sure that you're keeping your annotations up to date, because they're a way that you can be sure that your understanding of your own program is similarly up to date.