← Documents

lobee.ca · Documents

🐍 A Guide to CSC108 Python

Document icon with a code symbol on a navy nebula background

CSC108 is the University of Toronto's introductory programming course, taught in Python. This page is my study companion for it: one place for what the course tends to look for in your code, from naming and docstrings to the small bugs that cost the most marks.

Every code example here is original and generic, written just for this page. None of it comes from course materials, assignments or tests.

Style basics

Style marks are some of the easiest to keep. Past offerings have generally expected the following, and most of it comes straight from PEP 8, Python's own style guide.

  • Names in snake_case that say what they hold: total_price, not tp or x2.
  • Indent with 4 spaces. Don't use tabs.
  • Put spaces around operators and after commas: total = price * count.
  • Keep lines under 80 characters.
  • Give every function a docstring (see the recipe below).
  • When a question gives you a constant (an ALL_CAPS name), use it instead of retyping its value.
GoodPython
def average_speed(distance_km: float, hours: float) -> float:
    """Return the average speed in km/h of a trip that covered
    distance_km kilometres in hours hours.

    Precondition: hours > 0

    >>> average_speed(120.0, 2.0)
    60.0
    >>> average_speed(5.0, 0.5)
    10.0
    """
    return distance_km / hours
AvoidPython
def avg(d,h):
  return d/h

Some offerings have checked style automatically with a tool such as PyTA (python_ta). Whether yours does, and how much style is worth, varies, so check your course's rules.

The Function Design Recipe

The design recipe is the course's step-by-step way to write a function. It front-loads the thinking, so the body is usually the easy part.

  • 1. Examples. Write a couple of calls and what they should return. Pick examples that differ. For a function that returns a bool, show one True and one False.
  • 2. Header. Name the function and its parameters clearly, with type annotations such as list[str] or dict[str, int].
  • 3. Description. Start with a verb ("Return …"), say what it returns rather than how, and mention every parameter by name.
  • 4. Body. Write the code.
  • 5. Test. Run your examples, then try edge cases: an empty list, an empty string, a single item.

Worked example: counting long words

Say you need a function that counts how many words in a list are at least a given length.

Step 1: examples. Before any code, decide what a few calls should give back:

Python shellExamples first
>>> count_long_words(['owl', 'falcon', 'heron'], 5)
2
>>> count_long_words(['owl', 'emu'], 4)
0

Step 2: header. Choose clear names and annotate the types. The function takes a list of strings and an int, and returns an int:

Python
def count_long_words(words: list[str], min_length: int) -> int:

Step 3: description. Add the docstring: the examples from step 1, under a description that starts with a verb and names both parameters.

Python
def count_long_words(words: list[str], min_length: int) -> int:
    """Return the number of strings in words that have at least
    min_length characters.

    >>> count_long_words(['owl', 'falcon', 'heron'], 5)
    2
    >>> count_long_words(['owl', 'emu'], 4)
    0
    """

Step 4: body. Now the code. An accumulator (count) starts at 0 and grows inside the loop:

Pythonword_tools.py
def count_long_words(words: list[str], min_length: int) -> int:
    """Return the number of strings in words that have at least
    min_length characters.

    >>> count_long_words(['owl', 'falcon', 'heron'], 5)
    2
    >>> count_long_words(['owl', 'emu'], 4)
    0
    """
    count = 0
    for word in words:
        if len(word) >= min_length:
            count = count + 1
    return count

Step 5: test. Run the docstring examples (next sections show how), then poke at the edges:

Python shellEdge cases
>>> count_long_words([], 3)
0
>>> count_long_words(['kiwi'], 4)
1

Docstrings, type annotations and preconditions

  • Common annotations: int, float, str, bool, list[int], list[str], dict[str, int], and None for a function that returns nothing.
  • A function that returns a bool is usually described as "Return True if and only if …".
  • Describe the result, not the steps. "Return the number of …" beats "Loop over the list and add 1 …".
Python
def has_double_letter(word: str) -> bool:
    """Return True if and only if word has the same character twice
    in a row.

    >>> has_double_letter('coffee')
    True
    >>> has_double_letter('tea')
    False
    """
    for i in range(len(word) - 1):
        if word[i] == word[i + 1]:
            return True
    return False

A precondition is something the caller promises, so the function doesn't have to check it. Write it in the docstring, often as a Python expression; a question may ask for one written like 0 <= i < len(s).

Python
def swap_ends(text: str) -> str:
    """Return text with its first and last characters swapped.

    Precondition: len(text) >= 2

    >>> swap_ends('stop')
    'ptos'
    >>> swap_ends('ab')
    'ba'
    """
    return text[-1] + text[1:-1] + text[0]

Functions whose results aren't fixed, such as ones that use random, read input or only print, usually skip docstring examples.

Testing with doctest

Docstring examples double as tests. You can paste each call into your IDE's Python shell (the course has used Wing 101) and compare, or let Python check them all at once with the doctest module. Running doctest is a general good habit; it may not be something you're required to hand in.

Pythonword_tools.py (bottom)
if __name__ == '__main__':
    import doctest
    doctest.testmod()

No output means every example passed. To see each one run, add -v from a terminal:

Shell
python -m doctest -v word_tools.py

Trying calls by hand in the shell looks like this. Each line under a call is what Python printed back:

Python shell
>>> has_double_letter('coffee')
True
>>> has_double_letter('tea')
False
>>> has_double_letter('')
False
  • Examples must match the real output exactly: 'ptos' with quotes for a string, 60.0 rather than 60 for a float.
  • Use the function's real name in its examples.
  • One example is rarely enough. Cover the different cases the description mentions.
  • Other test tools such as pytest or unittest may or may not come up; check your course's rules.

Following question constraints

Questions often restrict how you may solve them: "use a for loop", "do not use any loops", "use the constant", "call your helper". Ignoring a restriction can cost the whole question even when the output is right. The same task can be correct one way and wrong another:

PythonIf asked: use a for loop
def total_letters(words: list[str]) -> int:
    """Return the total number of characters in the strings in words.

    >>> total_letters(['sun', 'moon'])
    7
    """
    total = 0
    for word in words:
        total = total + len(word)
    return total
PythonIf asked: no loops
def total_letters(words: list[str]) -> int:
    """Return the total number of characters in the strings in words.

    >>> total_letters(['sun', 'moon'])
    7
    """
    return len(''.join(words))

When a question tells you to use a helper function, call it. Don't copy its body:

Pythonhelper
def is_vowel(char: str) -> bool:
    """Return True if and only if char is a lowercase vowel.

    Precondition: len(char) == 1

    >>> is_vowel('e')
    True
    >>> is_vowel('z')
    False
    """
    return char in 'aeiou'
GoodPython
def starts_with_vowel(word: str) -> bool:
    """Return True if and only if word starts with a lowercase vowel.

    Precondition: word != ''

    >>> starts_with_vowel('apple')
    True
    >>> starts_with_vowel('pear')
    False
    """
    return is_vowel(word[0])
AvoidPython
def starts_with_vowel(word: str) -> bool:
    return word[0] in 'aeiou'
  • Paper tests have often come with a reference page of allowed built-ins and methods. Stick to what's on it.
  • On paper, partial code can still earn marks, and code is checked against the docstring examples, so write what you can.
  • Use parameters and return values, not global variables.
  • Functions that return a value generally shouldn't print or call input unless the question asks. Some offerings have disallowed both in graded functions; check your course's rules.
  • Import only what a question needs. File questions commonly use csv and TextIO from typing.
  • Whether break or particular built-ins are allowed varies. When in doubt, check.

Topic map: weeks 1 to 7

A rough order of topics. It can shift between terms, and this page only covers the first seven weeks.

  • Week 1: types; arithmetic with /, //, % and **; order of operations; variables and the memory model; built-in functions.
  • Week 2: defining functions, the design recipe, testing, string basics.
  • Week 3: bool, if / elif / else, short-circuit evaluation, choosing docstring examples, indexing and slicing.
  • Week 4: string methods, for and while loops, accumulators.
  • Week 5: lists and list methods, range, looping by item and by index, aliasing.
  • Week 6: parallel lists and nested lists.
  • Week 7: reading files (for line in f, readline, read, readlines, csv.reader) and dicts, including the dict accumulator.

Common pitfalls

The mistakes that come up again and again. In each pair the better version is on the left (on top on a phone).

print is not return

Printing shows a value in the shell, but the caller gets None. With the version on the right, double(4) + 1 crashes.

GoodPython
def double(n: int) -> int:
    """Return n multiplied by 2.

    >>> double(4)
    8
    """
    return n * 2
AvoidPython
def double(n: int) -> int:
    print(n * 2)

List methods change the list and return None

sort, append, extend, insert, remove and reverse change the list in place and return None. (pop is the exception: it returns the item it removed.) Use sorted(levels) if you want a new list.

GoodPython
levels = [3, 1, 2]
levels.sort()
# levels is now [1, 2, 3]
AvoidPython
levels = [3, 1, 2]
levels = levels.sort()
# levels is now None

Strings never change: keep the result

String methods return a new string. If you don't assign the result, it's lost.

GoodPython
greeting = '  hello  '
greeting = greeting.strip()
# greeting is now 'hello'
AvoidPython
greeting = '  hello  '
greeting.strip()
# greeting is still '  hello  '

Returning too early inside a loop

For an "every item" check, return False as soon as one item fails, and return True only after the loop. The version on the right only looks at the first item, and returns None for an empty list.

GoodPython
def all_positive(numbers: list[int]) -> bool:
    """Return True if and only if every number in numbers is
    greater than 0.

    >>> all_positive([3, 8, 1])
    True
    >>> all_positive([3, -8, 1])
    False
    """
    for number in numbers:
        if number <= 0:
            return False
    return True
AvoidPython
def all_positive(numbers: list[int]) -> bool:
    for number in numbers:
        if number <= 0:
            return False
        else:
            return True

Changing the loop variable does not change the list

number = number * 2 only rebinds the loop variable. To change the list, assign through an index.

GoodPython
def double_all(numbers: list[int]) -> None:
    """Modify numbers so that each item is doubled.

    >>> values = [1, 4]
    >>> double_all(values)
    >>> values
    [2, 8]
    """
    for i in range(len(numbers)):
        numbers[i] = numbers[i] * 2
AvoidPython
def double_all(numbers: list[int]) -> None:
    for number in numbers:
        number = number * 2

Put the guard first

and stops at the first False, so check that an index is valid before using it. The version on the right raises IndexError for char_is('loop', 9, 'o').

GoodPython
def char_is(text: str, i: int, char: str) -> bool:
    """Return True if and only if i is a valid index of text
    and text[i] is char.

    >>> char_is('loop', 2, 'o')
    True
    >>> char_is('loop', 9, 'o')
    False
    """
    return 0 <= i < len(text) and text[i] == char
AvoidPython
def char_is(text: str, i: int, char: str) -> bool:
    return text[i] == char and 0 <= i < len(text)

More to watch for

  • Slices and range stop before the stop value: 'python'[0:2] is 'py' and range(3) is 0, 1, 2.
  • Slicing past the end is safe ('abc'[1:10] is 'bc'), but indexing past it crashes ('abc'[10]).
  • A function with no return statement returns None.
  • b = a makes an alias, not a copy. Use a[:] to copy a list (next section).
  • Lines read from a file end in '\n', so strip them.
  • Values from csv.reader are strings. Convert with int or float before doing maths.
  • in on a dict checks the keys only, never the values.

Memory model and aliasing

A variable holds the id of an object (its memory address), not the object itself. Assignment copies the id. Two names that hold the same id are aliases: change a mutable object such as a list through one name, and the other name sees it.

Python shell
>>> a = [1, 2, 3]
>>> b = a
>>> b.append(4)
>>> a
[1, 2, 3, 4]
>>> c = a[:]
>>> c.append(5)
>>> a
[1, 2, 3, 4]
>>> a is b
True
>>> a is c
False
>>> a == c[:4]
True

is asks "same object?" (same id). == asks "equal values?". The same rule is why a function can change a list you pass to it, like double_all above: the parameter is an alias for the caller's list.

Strings, ints, floats and bools are immutable. Operations on them build new objects, so other names keep the old value:

Python shell
>>> word = 'cat'
>>> same = word
>>> word = word + 's'
>>> word
'cats'
>>> same
'cat'

Files and dicts quick reference

Reading files

A file function is usually handed a file that is already open. It reads from it and leaves opening and closing to the caller.

  • for line in f: loops over the lines, each ending in '\n'.
  • f.readline() reads one line; '' means you've reached the end.
  • f.read() returns the whole rest of the file as one string.
  • f.readlines() returns the rest as a list of lines.
  • csv.reader(f) gives each row as a list of strings (see the csv docs).
Python
from typing import TextIO


def read_nonblank_lines(text_file: TextIO) -> list[str]:
    """Return the lines of text_file that are not blank, with
    surrounding whitespace removed.
    """
    lines = []
    for line in text_file:
        line = line.strip()
        if line != '':
            lines.append(line)
    return lines

A CSV with a header row, and a dict accumulator that totals it:

CSVsightings.csv
species,count
heron,1
gull,4
heron,2
gull,3
Pythonbirds.py
import csv
from typing import TextIO


def tally_sightings(sightings_file: TextIO) -> dict[str, int]:
    """Return a dict that maps each species in sightings_file to the
    total number seen. sightings_file is an open CSV file with a
    header row, then rows of species,count.
    """
    sightings_file.readline()  # skip the header row
    totals = {}
    for row in csv.reader(sightings_file):
        species = row[0]
        count = int(row[1])  # CSV values are strings
        if species in totals:
            totals[species] = totals[species] + count
        else:
            totals[species] = count
    return totals
Python shell
>>> sightings_file = open('sightings.csv')
>>> tally_sightings(sightings_file)
{'heron': 3, 'gull': 7}
>>> sightings_file.close()

Dicts

Python shell
>>> stock = {'apple': 4, 'pear': 2}
>>> stock['plum'] = 7
>>> stock['apple']
4
>>> 'pear' in stock
True
>>> 2 in stock
False
>>> len(stock)
3
>>> for fruit in stock:
...     print(fruit, stock[fruit])
...
apple 4
pear 2
plum 7
  • d[key] = value adds a key or replaces its value. d[key] on a missing key raises KeyError.
  • key in d checks keys only. Loop with for key in d: and look values up with d[key].
  • Dict accumulator: check if key in d: and add to the value, otherwise start it with d[key] = … (as in tally_sightings).
  • More in the Python tutorial's data structures chapter.

Closing note

That's the core of it: follow the recipe, let your examples drive the code, and read every question's restrictions twice. If you spot something wrong here, the official course materials and your instructors always have the final word.

References: PEP 8, the doctest docs, the csv docs and the Python tutorial's data structures chapter.