Coding Standards
CIS 211 Style Guide for Python Programs
Most software development organizations have coding guidelines that constitute a house style. These may be based on some more widely used standard, but often they will also incorporate norms and preferences developed by a particular organization. Students who took CIS 210 at UO have used a style guide for that course, based on PEP 8 and extended with some additional rules suited to an introductory course.
Guidelines for CIS 211 projects are also based on PEP 8, but they differ in some ways from those used in CIS 210. The key differences are that CIS 211 style is designed to be more concise.
This document represents my best effort at defining style guidelines for CIS 211, but it may require revision. We may find that we need some additional rules, or that some existing rules are annoyingly strict, or that something is not as clear as it should be. Students are encouraged to suggest revisions.
Basics: When in doubt, follow PEP 8
PEP 8 is a widely used standard for Python code. It provides guidelines for all variety of matters from indentation to spacing in expressions.
PEP 8 refers to PEP 257 for docstring conventions. We will also follow PEP 257 conventions. We will not specify function and method type signatures in docstrings (see below for the alternative), but when appropriate we may use docstring comments to say more about arguments and return values.
Function headers
We will use Python type annotations (also known as type hints) to specify the signature (argument and return types) of functions and methods. Type annotations are a relatively new feature of Python, but they are supported well by PyCharm.
Type annotations make it possible to specify the signature of a function very compactly, compared to describing argument types in a docstring. Instead of this:
def frobnaz(n, s):
"""frob the naz.
:n: integer, frob it this many times
:s: string from which we extract the naz
:returns: string in which the naz has been frobbed
"""
we will write
def frobnaz(n: int, s: str) -> str:
"""returns copy s with its frob nazzed n times"""
An absent return type is equivalent to specifying that the
function or method returns None
.
Method headers
We write type contracts for the methods of classes almost
exactly as we would for a function, with two differences.
First, we do not give a type for the self
argument, as it
is implied by the class.
class Point:
"""An (x,y) coordinate pair of integers"""
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def move_to(self, new_x: int, new_y: int):
"""Change the coordinates of this Point"""
self.x = new_x
self.y = new_y
As with functions, the return type annotation -> None
may be
omitted for methods that do not return a value. Such methods
will generally be mutators, i.e., it may be assumed that they
modify the value of the self
object, like the
move_to
method above.
Methods that return a
value other than None
(and which therefore have the -> T
annotation for some type T
) should typically not modify
the value of the self
object. On rare occasions, concise and
efficient code may require a method to both return a
value and modify the self
object. On those occasions,
the header comment of the method should explicitly and
unambiguously indicate that the method modifies the object.
Forward type annotations
The second case in which we cannot
simply write the type of an argument or return value is
when that type is the class we are defining.
The type is considered
undefined until the class is complete.
Thus we cannot write
def __add__(self, other: Point) -> Point:
"""(x,y) + (dx, dy) = (x+dx, y+dy)"""
return Point(self.x + other.x, self.y + other.y)
because the Point
class does not exist yet. The workaround
is to place quotes around the type name to indicate that it
is a forward reference, i.e., a reference to a type that
we promise will exist soon even though it does not exist
quite yet.
def __add__(self, other: "Point") -> "Point":
"""(x,y) + (dx, dy) = (x+dx, y+dy)"""
return Point(self.x + other.x, self.y + other.y)
Composite type annotations
Types like int
and str
can be used directly in
type annotations. Types like tuple
and list
can also be
used in type annotations, but typically we will prefer to be
more specific, e.g., not just a tuple
but more precisely
tuple in which the first element is a string and the second
element is an int. We can make this more specific annotation
by importing type constructors from the typing
module:
from typing import Tuple, List, Dict
def i_eat_tuples(t: Tuple[str, int]) -> Tuple[int, Tuple[int, int]]:
the_string, the_int = t
return (the_int, (the_int, the_int))
def to_dict(ls: List[Tuple[str, int]]) -> Dict[str, int]:
result = { }
for key, value in ls:
result[key] = value
return result
Method headers
Method headers follow the same rules as function signatures with the following exceptions:
- The self argument of a regular method should not have a type annotation.
- Similarly, the cls argument of a class method should not have a type annotation.
Mutation
A function or method that returns a result should not modify its input arguments. For example, if I write this function header:
def jazziest(musicians: List[Musician]) -> Musician:
it is implicit that the input list of musicians is not modified.
Occasionally returning a result and modifying an argument in the
same function is helpful for writing concise and efficient code. The
pop
method for lists is an example of this. This is
permissible but must be clearly and explicitly documented in the
header function, and to the extent possible the function name should
suggest that it modifies its input. For example, if the above
function returned the jazziest musician an also removed it from the
list, the function header and docstring might look like this:
def pull_jazziest(musicians: List[Musician]) -> Musician:
"""Remove and return jazziest musician from musicians"""
Classes: Public and quasi-private attributes
In accordance with PEP 8, object fields and methods that are meant to be public (that is, accessed from code outside the class or its subclasses) should begin with a lowercase letter. Names of fields and methods that are not meant to be accessed directly by code outside a class should begin with a single underscore. (These fields and methods might be declared private in a language with encapsulation, such as Java or C++, but Python does not provide encapsulation.)
In addition to the access categories described in PEP 8, we add two that can only be specified in docstrings:
- A docstring may describe a class as immutable, in which case its public fields may be accessed but not modified by code outside the class.
- In a class that is not immutable, individual fields may be described as read-only, meaning that outside code may access them but must not directly change them.
Tests
We will not use doctests in CIS 211, and in most cases our docstrings will not include examples of use. Instead, we will use unit tests separate from the module code.
Unit tests may be included in the same source file as the code to
be tested, or it may be in a separate file. If it is in a separate
file, code to test code.py
should be named
test_code.py
.
We will use the Python unittest framework whenever practical. The unittest framework may not be suitable for testing some features, such as interactive input and output. In those cases, tests may be written using whatever means is appropriate, with as much test automation as feasible.
In many cases I will provide a starter test suite with a project. The provided test suite is never exhaustive, and passing the provided test cases is not a guarantee that a project is correct. Students are strongly encouraged to add their own test cases, and may sometimes be required to do so (that is, designing and implementing good test cases may be part of the project requirements).