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.
Living Document#
This document represents my best effort at defining style guidelines for CIS 211, but like many software documents it requires regular 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. Also I make mistakes. Students are encouraged to suggest revisions. Meanwhile, the standard for any particular project is the version of this document as of the project due date. If the guidelines change between distribution of a project and its due date, I will use Piazza to announce whether the change applies to the current project, and to the extent possible I will avoid making you revise projects that have already been started.
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.
Note in particular that we will follow PEP8 naming conventions,
including lower_case
for variables, functions, and methods,
CamelCase
for classes, and SHOUTS
for global constants.
PEP 8 recommends a maximum line length of 79 characters.
This matters! The human eye has limited accuracy as it moves from the
end of one line of text to the beginning of the next line. 79 is not a
magic number (accuracy depends on many other factors, such as vertical
space between lines), but under typical circumstances it is
approximately the length at which readers begin to have trouble finding
the beginning of the correct next line.
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 and method 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 follow the same rules as function signatures 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.
Mutation#
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.
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.
On rare occasions, concise and efficient code may require a method to
both return a
value and modify the self
object. The
pop
method for lists is an example of a mutator method that returns a
result. 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"""
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
Docstrings for functions and methods#
A docstring should provide information that is useful to a developer.
In particular, consider a developer who is building a new module that
uses the functions or classes in your module, or who has been asked to
add a feature to the code, fix a bug, or modify some functionality.
Avoid noisy repetition of information that is already present in type
hints, as well as information that is implicit. For example, the
developer does not need to be reminded that __init__
is the
constructor, but may benefit from explanation of the data
representation.
Some methods may not require docstrings at all when they follow Python conventions:
An
__init__
that simply copies its arguments into instance variables according to a systematic naming scheme (e.g., argumentx
might be copied intoself.x
orself._x
) may not require a docstring.An
__eq__
that checks for equality of some or all of its instance variables, with or without a type equivalence check (type(self) == type(other)
) may not require a docstring, or the docstring may briefly summarize which instance variables are considered in the equivalence check.A single line
__str__
or__repr__
may not require a docstring, but often it will be useful to document them with an example, particularly when the__repr__
does not look exactly like a call to the constructor.
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 (read-only), 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.
Avoid type and isinstance#
CIS 211 is a course in object-oriented programming. It emphasizes making use of dynamic dispatch. Students who are not yet comfortable with dynamic dispatch (method calls that may have different behavior depending on the class of the object on which they are called) often use the built-in functions isinstance or type to control behavior. Don’t do that. Assume that unless the instructions for a project or exam question specifically allow isinstance or type, you should be using dynamic dispatch instead. Inappropriate uses of isinstance or type will be marked as errors.
Exception: type comparison in __eq__
#
There is one blanket exception to the above rule about avoiding
type
: It is allowed and often a good idea to make an object
equivalence test (the __eq__
magic method) include a type equality
check. For example, consider a simple Point
class:
class Point:
"""2D cartesian point"""
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
return type(self) == type(other) and
self.x == other.x and self.y == other.y
Note that if we create a subclass Point3D
with a z coordinate, this
__eq__
method will determine that Point(5.0, 3.0, 7.0)
is not equal
to
Point3D(5.0, 3.0)
; without the type check it would consider them
equal.
Symbolic Constants vs Magic Values#
A magic number is a constant other than 0, 1, True
, False
, or
""
or []
that appears literally in code. For example, this snippet
contains a magic number:
area = radius * radius * 3.141592
For more explanation of magic numbers and why they are undesirable, see the “Unnamed numerical constants” section of the Magic Numbers article on Wikipedia. I use the concept here to refer not only to numeric constants, but also to strings that carry some special meaning in an application.
Very occasionally, if a constant is used only once, and if its meaning would be apparent to any developer reading the code, a magic constant might be acceptable. In general, though, magic numbers should be replaced by symbolic constants, e.g.,
# Put this near the beginning of the file
PI = 3.141592
...
# Then use the symbolic name throughout
area = radius * radius * PI
When a group of constants represent alternatives, often
they are best represented by an enumeration. Python supports
enumerations with the enum
package. The main advantage of
enumerations over other symbolic constants is in providing a type
that can be declared in type annotations.
import enum
class Color(enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
light: Color # Declare that variable light will only hold Color values
light = Color.red
If the same constant is used in more than one code file, and especially if it is a value that could conceivably be changed, it should be factored out into a separate source file. For example, if you are constructing a board game with multiple source files, the size of the game board probably belongs in a separate source file.
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 when practical. The unittest framework may not be suitable for testing some features, such as interactive input and output and simulation results. 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).