Python Style Guide
This guide has a variety of do's and don'ts for recommended style in Python programming!
Maintaining clean programmatic style is important for a variety of reasons:
It helps to keep your code readable, scalable, and is useful for collaboration in which others reading your code have expectations for conventional formats.
It helps to practice clean coding paradigms like "one change, one place" and avoid security exploits like restricting access to data members.
For the above reasons, it's important for job and internship recruiters, which means that sloppy code can tank your chances of nailing the interview!
As such, it's not just about making code that works, but also code that is well documented with clear identifiers, helper methods where needed, and a variety of other concerns that follow.
Basics
The following style advice applies to pretty much every programming language, but is demonstrated herein in Java.
Spacing & Indentation
First things first: USE ONLY SPACES TO INDENT (or only tabs, though that's less preferable), but NOT a mixture of both.
A couple of things you can use to verify this:
Google how to use your chosen IDE to replace all tabs with spaces for indentation.
In many development environments, you can search for tabs by enabling the "regular expression" search option and finding
\t
, making sure to replace these with 4 spaces wherever found.
Bad |
Good |
---|---|
Sloppy indentation makes code hard to follow and difficult to understand what statements belong to what blocks. |
All |
for i in range(n): for j in range(n): print(i) print(j) |
for i in range(n): for j in range(n): print(i) print(j) |
Bad |
Good |
---|---|
New lines/spaces should be used tactically; you should never have more than 2 new lines in a row for any reason. |
Use spaces sparingly, generally to separate only key blocks of code and typically variable declarations at the top of some block from where they're used. |
s = "test" a = 5 for i in range(len(s)): if s[i] == b: # ... do something |
s = "test" a = 5 for i in range(len(s)): if s[i] == b: # ... do something |
Variables, Naming, & Documentation
Bad Programming isn't like math in that you should feel bound to small variable names in an equation that are defined elsewhere. def d (vi: float, vf: float, t: float) -> float: return ((vi + vf)/2)*t |
Good Names of variables/methods/constants etc. should clearly indicate their purpose--use as much room as you need, though try to find a good balance between parsimony and clarity. def get_distance (velocity_initial: float, velocity_final: float, time: float) -> float: return ((velocity_initial + velocity_final)/2)*time |
Remember that in python, variables and attributes are named using snake_case
with all lower-case letters and underscores separating words,
but class names are distinguished by using PascalCase
, where each word is capitalized and there are no underscores separating words.
Bad |
Good |
---|---|
Literals (numbers like 5, 3.2, or String literals like "test") that may need to be changed later (especially if they are repeated often) can be hard to find and change accurately if they are left without being named. |
Instead, make these static constants that are easy to change in a single location, which also adds to the clarity of your code since you've now associated some meaning to a value. |
if someVar < 3 or someVar > 10: # ... do something |
LOWER_BOUND = 3 UPPER_BOUND = 10 # ... if someVar < LOWER_BOUND or someVar > UPPER_BOUND # ... do something |
Bad Methods that lack any sort of parameter and return type hints obscure their input-output formats, and lacking accompanying docstrings can further detract from easy plain-English explanations of their (1) purpose, (2) parameters, (3) return types, and (4) any other properties like errors thrown. def split_nums (listy): odds, evens = [], [] for i in listy: (odds if i % 2 == 1 else evens).append(i) return (odds, evens) |
Good ALL methods, be it helpers or publicly accessible ones, should have proper docstrings and type hints for parameters and return values, and in this class, we'll adopt the Google-style docstrings, that look like the following: def split_nums (listy: list[int]) -> tuple(list[int], list[int]): ''' Splits the given list of ints into the odds and evens. Parameters: listy (list[int]): A list of int values to be split into odds and evens. Returns: tuple(list[int], list[int]): A tuple of two sublists with index 0 containing the odd numbers and index 1 containing the evens from the input list. ''' odds, evens = [], [] for i in listy: (odds if i % 2 == 1 else evens).append(i) return (odds, evens) |
The same rule applies for documenting classes! Make sure you describe your attributes!
Conditionals
Bad |
Good |
---|---|
If you already have a boolean condition, you should *never* compare it to the boolean literals. |
Either use the boolean itself or use the negation operator |
inty = 2 booly = inty > 3 if booly == False: # ... do something |
inty = 2 booly = inty > 3 if not booly: # ... do something |
Iteration
Bad When possible, don't use a traditional for-loop to initialize a data structure like a listy = [] for i in range(something): listy.append(i) |
Good Using List Comprehension in this case can make more concise and readable code. listy = [i for i in range(something)] |
Advanced
This section is devoted to more class-design and architectural stylistic concerns.
Class Design
Bad |
Good |
---|---|
Classes used primarily as templates for their attributes (especially if there are many) can be syntactically cumbersome to maintain, as constructors will start to feel repetitive. |
Instead, declare these as a |
class DataNode: def __init__(attr1: int, attr2: int, attr3: str): self.attr1: int = attr1 self.attr2: int = attr2 self.attr3: str = attr3 # ... other methods here |
@dataclass class DataNode: attr1: int attr2: int attr3: str # ... other methods here |
Note: dataclasses provide *default* implementations of a variety of methods like __init__, __hash__, __eq__
that initialize / hash / compare ALL
attributes of the class. You'll need to override these methods if:
The constructor has some special handling of an attribute, like making a deep copy of the argument to the constructor to avoid aliasing.
Your
__eq__, __hash__
aren't meant to compare ALL of the attributes of the object.
Bad |
Good |
---|---|
Attributes are risky to expose to users, as they may change their values outside of your (the class designers') control, leading to unpredictable or insecure behavior. |
Although you cannot protect these with certainty in Python, attributes / methods that you wish to signal as "private" to the class' objects
(and thus, presumably, controlled / manipulated only through public methods called by the class' user) should begin with an underscore |
@dataclass class Person: # Attributes we wish to remain "private" age: int name: str |
@dataclass class Person: # Attributes we wish to remain "private" _age: int _name: str # Provide relevant getter methods if needed def get_age() -> int: return self._age # ... |
Bad |
Good |
---|---|
Using static (i.e., class) variables as a means of sharing data between method calls is generally a mistake -- it can lead to unpredictable behavior and bugs, especially if your methods are called in parallel (multi-threading is covered in your OS class, and exposes a risk that might not be obvious now). |
Static *constants* are OK, but if you need pieces of data to be passed between method calls, simply create a private helper method with all of the parameters needed or make each method accept any arguments they need (if allowed by the spec) instead of the static variable. |
class SomeClass: # [X] Don't do this shared_var = [] @staticmethod def someMethod1 (arg1: int) -> int: # ... shared_var.append(arg1) # ... @staticmethod def someMethod2 (arg1: int) -> int: # ... for i in shared_var: # ... |
class SomeClass: @staticmethod def someMethod1 (arg1: int, list_arg: list[int]) -> int: # ... list_arg.append(arg1) # ... @staticmethod def someMethod2 (arg1: int, list_arg: list[int]) -> int: # ... for i in list_arg: # ... |
...more to come!