Python Coding Style: Professional Guide
Write code that's readable, maintainable, and professional. Follow PEP 8 + modern best practices.
What It Is (Simple)
Think of it like grammar for code.
- PEP 8 = Official Python style guide
- Emphasizes readability over cleverness
- Everyone's code looks the same → easier collaboration
- Your linter (Ruff, Flake8) enforces these automatically
Why care?
- ✅ Code is read 10x more than written
- ✅ Team can maintain each other's work without confusion
- ✅ Professional, trustworthy appearance
- ✅ Easier refactoring and debugging
- ✅ Follows community standards (Python way)
Core Principles
| Principle | Rule | Example |
|---|---|---|
| Line Length | Max 88 characters (Black default) | No horizontal scrolling |
| Indentation | 4 spaces (never tabs) | def func():␣␣␣␣pass |
| Imports | Organized in 3 groups | Standard lib, 3rd-party, local |
| Naming | snake_case for vars/functions | user_name, not userName |
| Classes | PascalCase | class UserManager: |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES = 5 |
| Blank Lines | 2 between top-level defs, 1 between methods | Logical separation |
| Comments | Explain why, not what | ✅ Works because of async behavior |
| Docstrings | 3 quotes, at function start | Visible to help() and IDE docs |
Naming Conventions (Most Important)
Variables & Functions: snake_case
# ✅ CORRECT
user_name = "john"
total_count = 42
is_active = True
max_retries = 3
def get_user_data():
pass
def calculate_total_price(items):
return sum(item.price for item in items)
# ❌ WRONG
userName = "john" # camelCase (not Python)
TotalCount = 42 # PascalCase (for classes only)
isactive = True # No underscores (hard to read)
getUser = None # Unclear, looks like function name
Classes: PascalCase
# ✅ CORRECT
class UserManager:
pass
class WorkflowExecutor:
pass
class APIResponse:
pass
# ❌ WRONG
class user_manager: # Should be PascalCase
class userManager: # camelCase (use PascalCase)
class USERMANAGER: # ALL CAPS (reserved for constants)
Constants: UPPER_SNAKE_CASE
# ✅ CORRECT
MAX_RETRIES = 5
API_TIMEOUT = 30
DEFAULT_LOG_LEVEL = "INFO"
DATABASE_URL = "postgresql://..."
# ❌ WRONG
max_retries = 5 # Should be UPPER for constants
MaxRetries = 5 # PascalCase (use UPPER)
maxretries = 5 # No underscores
Private/Internal: Leading Underscore
# ✅ CORRECT
class UserManager:
def __init__(self):
self._internal_state = None # Private to class
self.__very_private = None # Name-mangled (rarely used)
def _helper_method(self): # Internal helper
pass
def public_method(self): # Public API
pass
Spacing (Horizontal)
Around Operators: Single Space
# ✅ CORRECT - space around operators
result = a + b * c
total = (first + second) / 2
is_valid = value >= threshold
config = {"timeout": 30, "retries": 5}
items = [1, 2, 3, 4]
# ❌ WRONG - no space or inconsistent
result=a+b*c
total=(first+second)/2
is_valid=value>=threshold
config={"timeout":30,"retries":5}
items=[1,2,3,4]
Function Calls: Space After Commas
# ✅ CORRECT
def calculate(x, y, z):
items = [1, 2, 3, 4]
data = {"name": "John", "age": 30}
return function_call(arg1, arg2, arg3)
# ❌ WRONG
def calculate(x,y,z):
items = [1,2,3,4]
data = {"name":"John","age":30}
return function_call(arg1,arg2,arg3)
No Spaces Inside Brackets/Parentheses
# ✅ CORRECT
my_list[0]
my_dict['key']
my_dict.get('name', 'Unknown')
result = function(arg1, arg2)
text.upper()
slice_data[1:5]
# ❌ WRONG
my_list[ 0 ]
my_dict[ 'key' ]
my_dict.get( 'name', 'Unknown' )
result = function( arg1, arg2 )
text.upper( )
slice_data[ 1 : 5 ]
Inline Comments: Two Spaces Before
# ✅ CORRECT
x = x + 1 # Increment counter
y = calculate_result() # Complex calculation
value = process_data() # Returns None on error
# ❌ WRONG
x = x + 1# Increment counter (no space)
y = calculate_result() # Complex calculation (just one space)
Spacing (Vertical)
Top-Level Definitions: Two Blank Lines
# ✅ CORRECT
import os
import sys
def first_function():
pass
def second_function():
pass
class MyClass:
pass
class AnotherClass:
pass
# ❌ WRONG
import os
import sys
def first_function():
pass
def second_function():
pass
class MyClass:
pass
Methods in Classes: One Blank Line
# ✅ CORRECT
class UserManager:
def __init__(self):
self.users = []
def get_user(self, user_id):
pass
def add_user(self, user):
pass
def delete_user(self, user_id):
pass
# ❌ WRONG
class UserManager:
def __init__(self):
self.users = []
def get_user(self, user_id):
pass
def add_user(self, user):
pass
Within Functions: Logical Grouping Only
# ✅ CORRECT - use blank lines to group logical sections
def process_workflow(workflow):
# Validation
if not workflow:
return None
# Processing
result = workflow.execute()
data = format_result(result)
# Logging
logger.info(f"Processed: {workflow.name}")
return data
Imports Organization
Group in this order:
- Standard library (os, sys, json, pathlib, etc.)
- Third-party (requests, django, fastapi, etc.)
- Local application (from . import config, etc.)
# ✅ CORRECT
# Standard library
import os
import sys
import json
from pathlib import Path
from typing import List, Dict, Optional
# Third-party
import requests
import numpy as np
from django.db import models
from fastapi import FastAPI, Request
# Local application
from myapp import config
from myapp.utils import helper_function
from myapp.models import User, Workflow
# ❌ WRONG (mixed order)
from myapp.models import User
import requests
import os
from myapp.utils import helper_function
import json
Use isort or Ruff to Automate
# Auto-organize imports
isort .
ruff check --fix .
Line Length: 88 Characters (Black Default)
Why 88?
- 79 is outdated (vintage terminals)
- 88 is modern (fits 2-column editors, CI logs)
- Reduces line breaks without excessive wrapping
- Enforced by Black (industry standard)
How to Handle Long Lines
# ✅ CORRECT - break logical sections
# Function call with many args
result = complex_function(
argument_one="value",
argument_two=123,
argument_three=True,
)
# Long string
message = (
"This is a very long message that would exceed "
"the line length limit if written on one line."
)
# Conditional
if (
user.is_active
and user.has_permission('admin')
and not user.is_suspended
):
process_user(user)
# Dictionary
config = {
"database_url": "postgresql://localhost/mydb",
"timeout": 30,
"max_retries": 5,
}
Use Black or Ruff to Auto-Format
# Auto-format to 88 chars
black .
ruff format .
Docstrings & Comments
Docstrings: Always Add to Public Functions/Classes
# ✅ CORRECT - Google-style docstring
def calculate_total_price(items: List[Item]) -> float:
"""Calculate total price for a list of items.
Args:
items: List of Item objects to sum.
Returns:
Total price as a float.
Raises:
ValueError: If items is empty.
"""
if not items:
raise ValueError("Items cannot be empty")
return sum(item.price for item in items)
# ❌ WRONG - no docstring
def calculate_total_price(items):
return sum(item.price for item in items)
Comments: Explain Why, Not What
# ✅ CORRECT - explains reasoning
# Retry with exponential backoff to avoid overwhelming the API
for attempt in range(MAX_RETRIES):
try:
return api.call()
except Exception:
time.sleep(2 ** attempt)
# Use list comprehension instead of loop for better performance
filtered = [x for x in data if x > threshold]
# ❌ WRONG - just restates code
# Loop through items
for item in items:
# Print item
print(item)
# Set x to 5
x = 5
Type Hints: Use Them
# ✅ CORRECT - type hints make intent clear
from typing import List, Dict, Optional
def process_workflow(
workflow: Workflow,
timeout: int = 30,
debug: bool = False,
) -> Optional[Result]:
"""Process a workflow with optional timeout."""
pass
class UserManager:
def __init__(self) -> None:
self.users: List[User] = []
def get_user(self, user_id: str) -> Optional[User]:
pass
def add_user(self, user: User) -> bool:
pass
Indentation: 4 Spaces, Never Tabs
# ✅ CORRECT - 4 spaces
def my_function():
if condition:
result = do_something()
if nested:
return result
class MyClass:
def method(self):
pass
# ❌ WRONG - tabs (invisible, causes issues)
def my_function():
→ if condition: # TAB character (bad!)
→ → result = do_something()
Configure editor:
- VS Code:
"editor.insertSpaces": true, "editor.tabSize": 4 - PyCharm: Settings → Editor → General → Spaces, Indent: 4
Real-World Example: Before & After
❌ BEFORE (Messy)
import requests
from myapp.models import user
import os
class userManger: # Wrong casing
def __init__(self):
self.users=[] # No spaces
def get_User(self,user_id): # Mixed casing
# Loop through users # Useless comment
for u in self.users:
if u['id']==user_id:
return u
def addUser(self,User): # Wrong naming
self.users.append(User)
✅ AFTER (Professional)
import os
import requests
from myapp.models import User
class UserManager:
"""Manages user operations."""
def __init__(self) -> None:
"""Initialize user manager with empty list."""
self.users: List[User] = []
def get_user(self, user_id: str) -> Optional[User]:
"""Get user by ID.
Args:
user_id: User identifier.
Returns:
User object if found, None otherwise.
"""
# Linear search is fine for small sets; consider indexing for scale
for user in self.users:
if user.id == user_id:
return user
return None
def add_user(self, user: User) -> None:
"""Add user to manager."""
self.users.append(user)
Auto-Formatting Setup (Eliminate Manual Work)
One-Command Setup
pip install black ruff isort
pyproject.toml
[tool.black]
line-length = 88
target-version = ["py38"]
[tool.isort]
profile = "black"
line_length = 88
[tool.ruff]
line-length = 88
target-version = "py38"
Use Pre-Commit Hooks
pip install pre-commit
pre-commit install
.pre-commit-config.yaml:
repos:
- repo: https://github.com/psf/black
rev: 23.0.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.0
hooks:
- id: ruff
args: [--fix]
Format Before Commit
black . && isort . && ruff check --fix .
Common Mistakes & Fixes
Mistake 1: Mixed Naming Conventions
# ❌ WRONG
userCount = 5 # camelCase (Python uses snake_case)
getUserData = None # Looks like function
TEMP = 10 # Not truly constant
# ✅ CORRECT
user_count = 5
get_user_data()
BUFFER_SIZE = 10
Mistake 2: No Type Hints
# ❌ WRONG
def process(data):
return data
# ✅ CORRECT
def process(data: List[Dict]) -> Optional[Result]:
return data
Mistake 3: Inconsistent Spacing
# ❌ WRONG
x=1+2*3
result = function(a,b,c)
# ✅ CORRECT
x = 1 + 2 * 3
result = function(a, b, c)
Mistake 4: Poor Import Organization
# ❌ WRONG
from myapp.models import User
import requests
import os
import json
# ✅ CORRECT
import json
import os
import requests
from myapp.models import User
Mistake 5: Meaningless Comments
# ❌ WRONG
x = 5 # Set x to 5
# Loop through items
for item in items:
print(item) # Print
# ✅ CORRECT
# Use batch size of 5 for API rate limiting
batch_size = 5
# Process items in order (preserve dependency chain)
for item in items:
print(item)
Quick Checklist
- ✅ Variable/function names:
snake_case - ✅ Class names:
PascalCase - ✅ Constants:
UPPER_SNAKE_CASE - ✅ Line length: 88 chars max
- ✅ Indentation: 4 spaces
- ✅ Space around operators:
a + b, nota+b - ✅ No spaces inside brackets:
func(arg), notfunc( arg ) - ✅ Two blank lines between top-level defs
- ✅ One blank line between methods
- ✅ Imports organized: stdlib, 3rd-party, local
- ✅ Type hints on public functions
- ✅ Docstrings on public functions/classes
- ✅ Comments explain why, not what
- ✅ Run
black .+ruff check --fix .before commit
One-Liner Automation
# Format everything in one go
black . && isort . && ruff check --fix .
# Then commit
git add .
git commit -m "style: auto-format code"
Resources
Final Principle
Code style is about respect: for your teammates, future self, and the Python community.
Write code you'd be proud to maintain 2 years later. Consistency beats perfection. Automate the boring parts.
Your stack (2026):
- Write code (follow conventions)
- Black formats it (one command)
- Ruff checks it (one command)
- Pre-commit enforces it (automatic)
- CI validates it (pipeline)
Result: Professional, readable, maintainable code. Every single time. 🚀