Skip to content

Python – Code Layout & Formatting

Python's whitespace is not just stylistic — indentation is syntactically significant. Consistent formatting is therefore not optional. At Cygnus Dynamics, black and isort handle formatting automatically so engineers can focus on logic, not style debates.

Automate everything on this page

Run black . to auto-format and isort . to sort imports. Both are integrated into our pre-commit hooks and CI pipeline. If a formatting rule on this page conflicts with what black produces, black wins — do not override it manually.

Indentation

Use 4 spaces per indentation level. Never use tabs. Python's syntax makes indentation meaningful — mixing tabs and spaces is a syntax error.

# ✅ 4-space indentation
def process_order(order: Order) -> None:
    if not order.items:
        raise EmptyOrderError("Order has no items")

    for item in order.items:
        if item.quantity <= 0:
            raise ValueError(f"Invalid quantity for item {item.id}")
        inventory.reserve(item)

# ❌ 2-space indentation (common in other languages — not Python)
def process_order(order):
  if not order.items:
    raise EmptyOrderError()

# ❌ Tabs
def process_order(order):
    if not order.items:     # tab character — mixes with spaces = SyntaxError
        raise EmptyOrderError()

Continuation Lines

When a statement must wrap across lines, use Python's implicit continuation inside parentheses, brackets, or braces — not backslashes. Align continuation lines either with the opening delimiter or use a hanging indent.

# ✅ Aligned with opening delimiter
result = some_long_function(argument_one, argument_two,
                            argument_three, argument_four)

# ✅ Hanging indent (preferred when signature is long)
def create_order(
        user_id: int,
        shipping_address: Address,
        payment_method: PaymentMethod,
        items: list[OrderItem],
) -> Order:
    ...

# ✅ Function call with hanging indent
order = order_service.create_order(
    user_id=current_user.id,
    shipping_address=validated_address,
    payment_method=payment_details,
    items=cart.items,
)

# ❌ Arguments on first line without alignment
order = order_service.create_order(user_id,
    shipping_address, payment_method)   # misaligned — visually confusing

Closing Brackets

The closing bracket of a multiline construct should line up under the first character of the line that starts the construct:

# ✅ Closing bracket under the first character of the line
my_list = [
    "item_one",
    "item_two",
    "item_three",
]

result = some_function(
    argument_one,
    argument_two,
)

Line Length

Maximum 79 characters per line. For docstrings and comments, limit to 72 characters.

Some teams agree to extend to 99 characters — this is acceptable at Cygnus Dynamics for application code, but comments and docstrings must always stay at 72.

Breaking Long Lines

Break before binary operators (Knuth style) — this keeps operators aligned at the left and makes it easier to spot which items are being combined:

# ✅ Break before operators — operators visible at left margin
total_income = (
    gross_wages
    + taxable_interest
    + (dividends - qualified_dividends)
    - ira_deduction
    - student_loan_interest
)

# ❌ Break after operators — operators lost at end of line
total_income = (gross_wages +
                taxable_interest +
                (dividends - qualified_dividends) -
                ira_deduction)

Use parentheses for implicit continuation — not backslashes:

# ✅ Parentheses — safe, clean
with (
    open("/path/to/input.csv") as input_file,
    open("/path/to/output.csv", "w") as output_file,
):
    process(input_file, output_file)

# ❌ Backslash — fragile (trailing space breaks it), avoid
with open("/path/to/input.csv") as f1, \
     open("/path/to/output.csv", "w") as f2:
    process(f1, f2)

Blank Lines

Location Blank Lines
Between top-level function and class definitions 2
Between method definitions inside a class 1
Between logically distinct sections within a function 1 (sparingly)
After the class declaration before the first method 0 — no blank line needed
import os


MAX_RETRIES = 3                      # ← two blank lines before first definition


class OrderService:                  # top-level class

    def get_order(self, order_id: int) -> Order:
        ...
                                     # ← one blank line between methods
    def create_order(self, request: CreateOrderRequest) -> Order:
        # 1. Validate
        self._validate_request(request)

        # 2. Build
        order = self._build_order(request)
                                     # ← one blank line between logical sections
        # 3. Persist
        saved = self._repository.save(order)
        return saved
                                     # ← one blank line between methods
    def _validate_request(self, request: CreateOrderRequest) -> None:
        ...


class PaymentService:                # ← two blank lines before next top-level class
    ...

Source File Encoding

All Python source files must be UTF-8. Do not add an encoding declaration — the absence of one implies UTF-8 in Python 3, which is correct.

# ❌ Unnecessary encoding declaration — omit it
# -*- coding: utf-8 -*-

# ✅ Nothing — UTF-8 is the default in Python 3

File Structure

Every Python module should follow this top-to-bottom order:

1. Module docstring
2. __future__ imports (if any)
3. Module-level dunders (__all__, __version__, __author__)
4. Imports — in three groups separated by blank lines
5. Module-level constants
6. Classes and functions
"""Order service — manages the full order lifecycle.

This module provides the OrderService class which coordinates
order creation, payment, and fulfilment.
"""

from __future__ import annotations

__all__ = ["OrderService"]
__version__ = "2.1.0"

# Standard library
import logging
from datetime import datetime
from typing import Optional

# Third-party
from sqlalchemy.orm import Session

# Internal
from cygnus.orders.models import Order
from cygnus.orders.exceptions import OrderNotFoundError
from cygnus.payments.gateway import PaymentGateway

# Module-level constants
logger = logging.getLogger(__name__)
DEFAULT_CURRENCY = "USD"


class OrderService:
    ...

Imports

One Import Per Line

# ✅ One module per import statement
import os
import sys
from pathlib import Path

# ✅ Multiple names from one module is acceptable
from collections import defaultdict, OrderedDict
from typing import Any, Optional, Union

# ❌ Two modules on one line
import os, sys

Import Order — Three Groups

Separate each group with one blank line:

# Group 1 — Standard library
import json
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

# Group 2 — Third-party packages
import boto3
import pydantic
from fastapi import FastAPI, HTTPException
from sqlalchemy.orm import Session

# Group 3 — Internal (Cygnus Dynamics)
from cygnus.common.exceptions import ValidationError
from cygnus.orders.models import Order
from cygnus.orders.repository import OrderRepository

Absolute Over Relative Imports

Prefer absolute imports — they are more readable and unambiguous. Use explicit relative imports only within a package when the absolute path would be unnecessarily verbose.

# ✅ Absolute import — clear and unambiguous
from cygnus.orders.repository import OrderRepository

# ✅ Explicit relative import — acceptable within the same package
from .repository import OrderRepository
from ..common.validators import validate_email

# ❌ Wildcard import — pollutes namespace, confuses static analysis
from cygnus.orders.models import *

Module-Level Dunders

Module-level dunders (__all__, __version__, __author__) go after the module docstring but before imports (except from __future__ imports):

"""Module docstring."""

from __future__ import annotations  # must be first

__all__ = ["OrderService", "OrderRepository"]
__version__ = "2.1.0"

import os  # regular imports after dunders

String Quotes

PEP 8 does not mandate single or double quotes — pick one style and apply it consistently across the project. At Cygnus Dynamics:

  • Double quotes " for all string literals (aligns with black's default)
  • Triple double quotes """ for all docstrings
# ✅ Double quotes — consistent with black
name = "Alice"
status = "CONFIRMED"
message = f"Order {order_id} has been confirmed."

# ✅ Use the other quote style to avoid escaping
label = 'It\'s confirmed'   # ❌ needs escape
label = "It's confirmed"    # ✅ no escape needed

# ✅ Triple double quotes for docstrings — always
def process_order(order: Order) -> None:
    """Process and persist the given order."""
    ...

Whitespace Rules

Avoid Extra Whitespace

# ✅ No space inside brackets/parentheses/braces
result = process(items[0], {"key": value})

# ❌ Extra spaces inside
result = process( items[ 0 ], { "key": value } )

# ✅ No space before colon, comma, semicolon
if x == 4:
    print(x, y)

# ❌ Space before colon/comma
if x == 4 :
    print(x , y)

# ✅ No space before opening parenthesis
calculate_total(items)
values[0]

# ❌ Space before parenthesis
calculate_total (items)
values [0]

Around Operators

# ✅ Single space around binary operators
total = price + tax
is_eligible = user_age >= MINIMUM_AGE and has_consent

# ✅ No space around = in keyword arguments or unannotated defaults
def create_order(user_id, currency="USD", notify=True):
    pass

result = create_order(user_id=42, currency="EUR")

# ✅ Space around = when combining annotation with default
def create_order(user_id: int, currency: str = "USD") -> Order:
    pass

# ✅ Space around -> in return annotation
def get_user(user_id: int) -> User:
    pass

# ❌ No space around assignment operator
total=price+tax

# ❌ Space around = in keyword arguments
result = create_order(user_id = 42)

Slice Notation

In slices, the colon acts as a binary operator. Treat it consistently — equal spacing on both sides, or no spacing when the parameter is omitted:

# ✅ Correct slice whitespace
items[1:9]
items[1:9:3]
items[:9]
items[1:]
items[lower:upper]
items[lower + offset : upper + offset]   # space when operands are complex

# ❌ Inconsistent slice spacing
items[1 :9]
items[1: 9]
items[lower+offset:upper+offset]   # no space makes it hard to see the boundary

Compound Statements

Each statement must be on its own line. Never put multiple statements on one line.

# ✅ One statement per line
if order.is_empty():
    raise EmptyOrderError()

for item in order.items:
    inventory.reserve(item)

# ❌ Multiple statements on one line
if order.is_empty(): raise EmptyOrderError()
for item in order.items: inventory.reserve(item)

# ❌ Definitely not
if order.is_empty(): raise EmptyOrderError()
else: process(order)

Trailing Commas

Use trailing commas in multi-line sequences, argument lists, and import blocks. They make version-controlled diffs cleaner — adding a new item only touches one line.

# ✅ Trailing comma in multi-line structures — cleaner diffs
SUPPORTED_CURRENCIES = [
    "USD",
    "EUR",
    "GBP",
    "AED",   # <- add new entry here, comma already present on previous line
]

def create_order(
    user_id: int,
    shipping_address: Address,
    items: list[OrderItem],   # <- trailing comma
) -> Order:
    ...

# ✅ Mandatory for single-element tuples
single_item = ("value",)   # without comma this is just a string in parentheses

# ❌ Trailing comma on same line as closing delimiter (except single-element tuple)
CURRENCIES = ["USD", "EUR", "GBP",]   # trailing comma on same line — avoid

Every Python project at Cygnus Dynamics must have these configured:

Tool Purpose Config
black Auto-formatter — opinionated, no config needed pyproject.toml
isort Import sorter — compatible with black pyproject.toml
flake8 PEP 8 linter .flake8
pylint Deeper code analysis .pylintrc
mypy Static type checker mypy.ini or pyproject.toml
pre-commit Git hooks for all of the above .pre-commit-config.yaml

Standard pyproject.toml configuration:

[tool.black]
line-length = 99
target-version = ["py311"]

[tool.isort]
profile = "black"
line_length = 99

[tool.mypy]
python_version = "3.11"
strict = true
ignore_missing_imports = true

Standard .pre-commit-config.yaml:

repos:
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black

  - repo: https://github.com/PyCQA/isort
    rev: 5.13.2
    hooks:
      - id: isort

  - repo: https://github.com/PyCQA/flake8
    rev: 7.0.0
    hooks:
      - id: flake8