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 blocks of code between curly brackets should be indented by 4 spaces, and adding 4 more in each nested block.

  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:

Google-style Docstrings


  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 not to flip as needed.

  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 list, set, dict when instead you can harness Python's comprehension syntax (see any tutorial on the matter like by asking ChatGPT or: some tutorial I just randomly Googled)

  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 @dataclass, which requires you to only mention what attributes the objects have without need to manually define the constructor (which will be provided for you instead, expecting constructor arguments in the same order as the attributes are listed).

  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!



  PDF / Print