A diagram showing the order of imports, constants, classes

Tips for Organizing Code in a Python File

Published at Aug 11, 2024

tl;dr: A source file should be organized into four sections: imports, constants, classes, and functions.

Motivation

I’ve rarely seen code review comments asking to change the structure of a file (e.g. “move this function to the bottom of the file”).

A couple guesses as to why no one talks about file structure in PR review comments:

  1. Missing strong enough norms around file layout to justify the effort of enforcing them.
  2. A reviewer would need to read the entire file to have a strong opinion about its structure. For large files, this is a lot of effort.
  3. Cleaning up the entire file is typically out of scope for a focused PR.
  4. The structure of a file either doesn’t matter much or is already considered good enough.

The goal of this blog post is to address #1 by advocating for stronger norms around a default file layout.

Tip #1: Consistency!

Be consistent with your file layout across the codebase. This is the most important tip. Readers should be able to pick up on structural patterns and understand how to quickly find what they’re looking for.

Tip #2: Sort functions from most important to least important

Putting the file’s public API at the top will immediately tell the reader what they can expect to find in the file.

Importance is hard to define, but a good proxy is counting how often it’s used outside of the file. A heavily used function is probably important.

Good example:

def create_user():
  ...
def _validate_user_data():
  ...

Bad example:

def _validate_user_data():
  ...
def create_user():
  ...

Tip #3: Classes should be defined before the functions.

This helps readers understand the flow of data through the program. This tip assumes the class is relevant to the functions (often by grouping together related pieces of data).

If a file feels more cohesive when interleaving classes and functions, consider splitting the file up. Don’t be afraid to make a separate file for a class if it doesn’t fit.

Good example:

@dataclass
class Credentials:
  username: str
  password: str
  api_token: Optional[str]
  refresh_token: Optional[str]

def validate(c: Credentials):
  ...

Tip #4: Imports and constants at the top.

This is already a fairly common pattern. Imports define symbols that are available in the rest of the file, and constants offer control points to change the program’s behavior.

Imports and constants are easy to skim quickly and often need to be defined early in the program to satisfy the Python interpreter.

Good example:

import pathlib
FILE_LIMIT = 1000

def search_files(path: pathlib.Path):
  ...