Skip to content

Style

Code that works isn't automatically code that's easy to live with — following a shared set of conventions is what keeps a Python file manageable as it grows past a few lines.

  • Consistency — code that follows the same conventions everywhere reads the same, no matter who wrote which part
  • Faster to learn — a new file feels familiar instead of like starting over, since the same patterns keep repeating
  • Easier to debug — a predictable layout means you already know where to look when something breaks
  • Easier for others to work with — working on a collaborative project means others review your code before approving it and merge it in with everyone else's changes; consistent style means they can focus on what you actually changed, instead of different formatting choices

Checklist

A few things worth double-checking before calling a script finished — each links to the full rule further down this page. Run a linter first, since it catches most of this automatically; what's left afterward has to be checked by eye.

Linter tool

A linter is a tool that scans your code and flags issues like PEP 8, Python's official style guide, and Pythonic idioms automatically. It reads your file, checks it against its rule set, and prints a report: one line per violation, giving the file, line number, a rule code, and a short message.

It can't catch a bug that only shows up when the code actually runs, since it never runs it.

A formatter tool (either separate, or a combined linter+formatter), actually rewrites your file on its own fixing the errors. However, it can be helpful to manually fix the issues on your own, so you learn to write them correctly for next time.

Comparing different tools

Tool Type Best for
PyCharm's built-in inspections Linter No setup needed — catches most PEP 8 violations and several Pythonic issues automatically
Pylint Linter Comprehensive checks — catches complex logical errors, not just formatting
Ruff Linter & Formatter Speed — large projects or CI pipelines where Pylint's speed becomes noticeable
Black Formatter Eliminating style debates entirely — rewrites the file to a consistent style automatically, instead of just flagging issues

Get started in your environment

Environment Installing third party tools Using a linter Using a formatter
PyCharm Settings > Plugins > tool name, then restart Built-in inspections run automatically, no setup needed; plugins do too, once installed. Underlines issues, hover for full message. Full issue list in View > Tool Windows > Problems. Code > Format Code
VS Code View > Extensions > tool name Underlines issues, hover for full message. Full issue list in View > Problems. Trigger via Format Document, or set it as the default formatter in settings.json
Outside of an IDE Send in terminal: pip install [tool name] print report in the terminal:
  • pylint your_file.py
  • ruff check your_file.py
rewrite the file directly:
  • black your_file.py
  • ruff format your_file.py

PEP 8 style guide

PEP 8 is Python's official style guide — a document written by Python's own core developers covering formatting, naming, and organizing code. "PEP" stands for Python Enhancement Proposal.

Python runs styled and unstyled code identically, so following PEP 8 doesn't make a script more correct — it makes it more predictable to read. Anyone who's used Python before recognizes the shape of PEP 8-styled code, so sticking to it means less friction reading someone else's code, and less friction when someone else reads yours.

File order

A Python file conventionally follows the same layout, top to bottom.1

  1. Module docstring — what the file does
  2. Imports — standard library, then third-party, then local
  3. ConstantsALL_CAPS values used throughout the file
  4. Functions and classes — the file's actual logic
  5. The if __name__ == "__main__": guard — the code that runs when the file is executed
"""
snake_survey.py

Tracks species and lengths recorded during the spring snake survey.

Author: Jordan Lee
Date: 2024-03-15
"""

import csv

MAX_TYPICAL_LENGTH_FT = 5

def is_unusually_long(length_ft):
    """Check whether a snake is unusually long for its species."""
    return length_ft > MAX_TYPICAL_LENGTH_FT

class Snake:
    """A single snake recorded during the survey."""

    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

if __name__ == "__main__":
    ball = Snake("ball python", 4.5)
    print(is_unusually_long(ball.length_ft))

Naming

A variable name should say what it holds — length_ft over l, species_list over data. snake_case and the other naming rules are covered on the Foundations page; this is about picking a meaningful name within those rules, not just a valid one.

l = 4.5                # what is l?
length_ft = 4.5        # clear at a glance

A short name is fine when its scope is short too — for s in species: is common, since s only exists for the one line inside the loop.

Constants

A constant is a variable whose value isn't meant to change while the program runs — written in ALL_CAPS by convention, so it's easy to tell apart from a regular variable at a glance. Defining one instead of repeating a raw number (a "magic number") gives that number a name explaining what it means.

if length_ft > 5:                    # what's special about 5?
    print("unusually long")

MAX_TYPICAL_LENGTH_FT = 5            # named once, explains itself
if length_ft > MAX_TYPICAL_LENGTH_FT:
    print("unusually long")

Constants are usually defined near the top of a file, so they're easy to find and adjust later — see File Order above.

Quote style

Python treats 'single' and "double" quotes identically for strings — PEP 8 doesn't prefer one over the other, just pick one as your default and stick with it throughout a file, rather than mixing both without reason. (This site uses double quotes.) The one except‌ion: switch to the other quote character for a string that itself contains a quote, rather than escaping it with a backslash.

print("it's a ball python")    # no backslash needed
print('it\'s a ball python')   # works, but harder to read

Docstrings

A triple-quoted string as the first line of a function or a file documents what it does — the underlying trick is the same multi-line comment covered on Foundations, just placed specifically as the first line.

def is_unusually_long(length_ft):
    """Check whether a snake is unusually long for its species."""
    return length_ft > 5

For a function whose parameters or return value need explaining, spell them out with a standard Args/Returns format instead of a one-line summary:

def is_unusually_long(species, length_ft):
    """
    Check whether a snake is unusually long for its species.

    Args:
        species (str): the snake's species name.
        length_ft (float): the snake's measured length, in feet.

    Returns:
        bool: True if length_ft is unusually long for species.
    """
    return length_ft > 5

Full rules on the Functions page.

Placed as the very first line of a file instead, the same trick becomes a module docstring — documenting the file as a whole rather than a single function, and a common place to note who wrote it and when.

"""
snake_survey.py

Tracks species and lengths recorded during the spring snake survey.

Author: Jordan Lee
Date: 2024-03-15
"""

species = "ball python"
length_ft = 4.5

Indentation

Python uses indentation, not braces, to mark a block — PEP 8's rule is 4 spaces per level, never tabs (mixing the two causes real errors, not just style complaints).

def describe(species):
  return f"a {species} python"      # 2 spaces — works, but not PEP 8

def describe(species):
    return f"a {species} python"    # 4 spaces — PEP 8

Blank lines

Two blank lines separate top-level function and class definitions; one blank line separates methods inside a class.

def parse_entry(text):
    ...
def save_entry(entry):    # only one blank line — not PEP 8
    ...


def parse_entry(text):
    ...


def save_entry(entry):    # two blank lines — PEP 8
    ...

Whitespace

Put a single space around most operators (=, ==, +, >), but drop it around = when it's a keyword argument rather than an assignment.

length_ft=4.5                              # missing spaces — not PEP 8
length_ft = 4.5                            # PEP 8

def describe(species, length_ft = 4.5):    # spaces around a keyword default — not PEP 8
    ...

def describe(species, length_ft=4.5):      # PEP 8
    ...

Comments

An inline comment needs at least two spaces before the # and one space after it; a block comment on its own line follows the same one-space-after rule.

length_ft = 4.5 #too short         # not PEP 8 — no spacing
length_ft = 4.5  # too short       # PEP 8 — two spaces before, one after

#check length                      # not PEP 8
# check length                     # PEP 8

Pythonic patterns

Pythonic code uses Python's own built-in features and standard patterns, instead of verbose work arounds.

There's no single tool that reliably flags all "unpythonic" code the way PEP 8 has a document to check against. The real habit is asking "does Python already have a built-in way to do this?" before writing a manual loop, counter, or flag — an instinct built over time to recognize the built-in pattern.

Other programming langues have different features and patterns, so if code is translated from another langauge into Python it might not be written very clearly. Pythonic code tends to be less buggy and faster.

Common patterns

A few of these a beginner tends to write out longhand before learning the built-in shortcut, roughly most to least common:

  • Truthy checks instead of len(x) > 0 — test a collection directly; a non-empty list is already truthy

    if len(species) > 0:    # works, but not Pythonic
        print("found some")
    
    if species:              # Pythonic — a non-empty list is already truthy
        print("found some")
  • enumerate() instead of range(len(...)) — loop with both the index and the item at once, instead of indexing into the list by hand

    for i in range(len(species)):        # manual indexing
        print(i, species[i])
    
    for i, s in enumerate(species):      # Pythonic — enumerate() hands back both
        print(i, s)
  • with open(...) instead of a manual open()/close() pair — a context manager guarantees the file gets closed even if something goes wrong partway through

    file = open("notes.txt")           # works, but there's a risk of locking the file in a buffer
    contents = file.read()
    file.close()
    
    with open("notes.txt") as file:    # Pythonic — closes automatically, even on error
        contents = file.read()
  • is None instead of == None — checking against None is a check of identity, not equality, so is is the correct tool

    length_ft = None
    if length_ft == None:                # works, but not Pythonic
        print("unknown length")
    
    if length_ft is None:                # Pythonic — `is` is the correct tool for a None check
        print("unknown length")
  • Avoid mutable default arguments — a default list or dict is created once, when the function is defined, and reused across every call — so items appended in one call are still there the next time, unless the default is None instead

    def add_snake(species, tracked=[]):     # works, but tracked is shared across every call
        tracked.append(species)
        return tracked
    
    def add_snake(species, tracked=None):   # Pythonic — a fresh list every call
        if tracked is None:
            tracked = []
        tracked.append(species)
        return tracked
  • Tuple unpacking instead of a temporary variable — swap two variables directly, rather than juggling a spare variable to hold one during the swap

    a, b = "ball python", "boa"
    temp = a                             # manual swap using a spare variable
    a = b
    b = temp
    
    a, b = b, a                          # Pythonic — tuple unpacking swaps directly

Additional best practices

Keep functions focused

A function should do one thing. If you find yourself describing it with "and" — "parses the input and saves it and prints a summary" — it's probably three functions.

def parse_and_save(text):    # doing too much
    ...

def parse_entry(text):       # one job each
    ...

def save_entry(entry):
    ...

Repeating the same few lines in multiple places is a sign to pull them into their own function instead — commonly called DRY ("don't repeat yourself"). It also means a fix only has to happen in one place, instead of every place the lines were copied to.

Guard clauses: return early instead of nesting

Handle the exception case first and return, rather than wrapping the rest of the function in an else. It keeps the normal path at the lowest indentation level, instead of nested one level deeper for every added check.

def describe(length_ft):
    if length_ft > 0:
        return f"{length_ft} ft"
    else:
        return "unknown length"
def describe(length_ft):
    if length_ft <= 0:
        return "unknown length"
    return f"{length_ft} ft"

Both versions do the same thing — the second reads top to bottom without having to track which if branch you're inside.

Catch specific exceptions

Catch the exact exception you expect (except ValueError:) instead of a bare except: — a bare except also silently swallows errors you didn't anticipate, including a typo in your own code, and even catches things like a keyboard interrupt (Ctrl+C) that usually shouldn't be caught at all. Full try/except mechanics are covered on the Errors page.

try:
    length_ft = float(user_input)
except:                    # catches everything, even mistakes you didn't expect
    print("invalid input")

try:
    length_ft = float(user_input)
except ValueError:         # only catches what you actually expect
    print("invalid input")

Type hints

A type hint annotates a parameter or return value with the type it's expected to be — species: str, length_ft: float, -> bool — without Python enforcing it at runtime; it's documentation an editor or a separate type checker (like mypy) can check for you.

def is_unusually_long(species: str, length_ft: float) -> bool:
    return length_ft > 5

A wrong type still runs — Python doesn't stop you from calling is_unusually_long("ball python", "4.5") with a string instead of a float — the hint only helps a tool catch the mismatch before you do, and helps a reader (or their editor) see what's expected without reading the function body.

Readable print output

\n and \t are escape sequences\n inserts a line break, \t a tab — so a single print() call can space out multi-line or columned output.

print(f"species: burmese\nlength: 10 ft\n")
print("species\t\tlength_ft")
print("ball python\t4.5")

A row of repeated characters makes a quick visual separator between sections of console output, useful for breaking up a long script's output into readable chunks.

print("survey results")
print("=" * 40)

Going further

Be creative with ASCII art

Write in the terminal with bubble letters or draw images through creative character use.

============================
                                ,----,                                       
,-.----.                      ,/   .`|       ,--,    ,----..            ,--. 
\    /  \                   ,`   .'  :     ,--.'|   /   /   \         ,--.'| 
|   :    \         ,---,  ;    ;     /  ,--,  | :  /   .     :    ,--,:  : | 
|   |  .\ :       /_ ./|.'___,/    ,',---.'|  : ' .   /   ;.  \,`--.'`|  ' : 
.   :  |: | ,---, |  ' :|    :     | |   | : _' |.   ;   /  ` ;|   :  :  | | 
|   |   \ :/___/ \.  : |;    |.';  ; :   : |.'  |;   |  ; \ ; |:   |   \ | : 
|   : .   / .  \  \ ,' '`----'  |  | |   ' '  ; :|   :  | ; | '|   : '  '; | 
;   | |`-'   \  ;  `  ,'    '   :  ; '   |  .'. |.   |  ' ' ' :'   ' ;.    ; 
|   | ;       \  \    '     |   |  ' |   | :  | ''   ;  \; /  ||   | | \   | 
:   ' |        '  \   |     '   :  | '   : |  : ; \   \  ',  / '   : |  ; .' 
:   : :         \  ;  ;     ;   |.'  |   | '  ,/   ;   :    /  |   | '`--'   
|   | :          :  \  \    '---'    ;   : ;--'     \   \ .'   '   : |       
`---'.|           \  ' ;             |   ,/          `---`     ;   |.'       
`---`            `--`              '---'                     '---'         
============================
Welcome to the program!
Press Enter:

ascii text resource

ascii art resource


  1. The first three steps are PEP 8. Where functions/classes and the main guard fall isn't PEP 8 — but it is the convention the rest of the Python community has settled on.