02 Exercise: PEP 8 - Level 4: Before/After Comparison
October 2025 (3886 Words, 22 Minutes)
Introduction
Welcome to Level 4 PEP 8 Exercises: Before/After Comparison! This advanced exercise set helps you understand the real-world impact of PEP 8 compliance by comparing non-compliant code with its corrected version.
Learning Objectives:
- Identify specific differences between non-compliant and compliant code
- Understand the reasoning behind each PEP 8 rule
- Recognize how small changes improve readability significantly
- Think critically about additional improvements beyond basic compliance
- Develop code review skills for production environments
Instructions:
- Study both the “Before” and “After” versions carefully
- Identify ALL differences between the two versions
- Answer the questions about which PEP 8 rules are being applied
- Consider what additional improvements could be made
- Click “Show Answer” to compare your analysis
Difficulty: Advanced
Time: 10-15 minutes per exercise
Prerequisites: Complete Level 1 and Level 2 exercises first
Why This Exercise Matters: In real-world development, you’ll often review pull requests or refactor existing code. This exercise trains you to spot PEP 8 violations in context and articulate why the corrected version is better—essential skills for code reviews and technical discussions.
Exercise 4.1: Simple Function
Before
def calc(a,b):return a+b
After
def calc(a, b):
return a + b
Questions
- List all differences between Before and After
- Which specific PEP 8 rules make the After version better?
- Are there any additional improvements you would suggest?
💡 Show Answer
Differences
- Space added after comma in parameters:
a,b→a, b - Function body moved to new line instead of same line as
def - Spaces added around
+operator:a+b→a + b
PEP 8 Rules Applied
- Use spaces after commas in parameter lists
- Avoid multiple statements on one line (compound statements discouraged)
- Surround binary operators with spaces
Additional Improvements
- Function name
calcis vague; better:calculate_sumoradd_numbers - Missing docstring
- Missing type hints:
def calculate_sum(a: float, b: float) -> float: """Return the sum of two numbers.""" return a + b
Key Takeaway
Even simple one-liners benefit from PEP 8 compliance. The After version is easier to read, debug, and maintain.
Exercise 4.2: Import Statements
Before
import sys,os,json
from typing import List,Dict
import numpy as np
After
import json
import os
import sys
from typing import Dict, List
import numpy as np
Questions
- List all differences between Before and After
- Which specific PEP 8 rules make the After version better?
- Why is the blank line in the After version important?
💡 Show Answer
Differences
- Each import on separate line
- Standard library imports alphabetically sorted (
json,os,sys) - Spaces added after commas in
fromimport:List,Dict→Dict, List - Typing imports alphabetically sorted:
List,Dict→Dict, List - Blank line separating standard library from third-party imports
- Imports grouped by type (standard library, then third-party)
PEP 8 Rules Applied
- Imports should usually be on separate lines
- Imports should be grouped: standard library, related third-party, local application/library
- Imports within each group should be alphabetically sorted
- Use spaces after commas
- Put a blank line between different import groups
Why the Blank Line Matters
It visually separates different import categories, making it immediately clear which imports are:
- Built-in Python modules (standard library)
- External dependencies (third-party packages)
- Local application code
This helps with:
- Dependency tracking: Easy to see what external packages are required
- Maintenance: Quick identification of what can be removed
- Debugging: Understanding which imports might cause issues in different environments
Key Takeaway
Import organization seems trivial, but in large projects with hundreds of imports, proper grouping and sorting saves significant time and prevents errors.
Exercise 4.3: Class Definition
Before
class dataProcessor:
def __init__(self,items):
self.items=items
self.count=len(items)
def Process(self): return [x*2 for x in self.items]
After
class DataProcessor:
def __init__(self, items):
self.items = items
self.count = len(items)
def process(self):
return [x * 2 for x in self.items]
Questions
- List all differences between Before and After
- Which specific PEP 8 rules make the After version better?
- What additional improvements would make this code production-ready?
💡 Show Answer
Differences
- Class name changed from snake_case to PascalCase:
dataProcessor→DataProcessor - Space added after comma in
__init__parameters:self,items→self, items - Spaces added around
=in assignments:self.items=items→self.items = items - Method name changed from PascalCase to snake_case:
Process→process - Method body moved to separate line (no compound statement)
- Blank line added between methods
- Spaces added around
*operator in list comprehension:x*2→x * 2
PEP 8 Rules Applied
- Class names should use PascalCase (CapWords convention)
- Method names should use snake_case (lowercase with underscores)
- Spaces after commas
- Spaces around operators (assignment and arithmetic)
- Avoid compound statements (multiple statements on one line)
- Blank line between methods in a class
Production-Ready Improvements
from typing import List
class DataProcessor:
"""Process and transform numerical data."""
def __init__(self, items: List[float]):
"""
Initialize processor with items.
Args:
items: List of numerical values to process
"""
self.items = items
self.count = len(items)
def process(self) -> List[float]:
"""
Double each item in the list.
Returns:
List of processed values (each item multiplied by 2)
"""
return [x * 2 for x in self.items]
Additional improvements include:
- Class docstring explaining purpose
- Type hints for parameters and return values
- Method docstrings with Args and Returns sections
- More descriptive method name (e.g.,
double_valuesinstead of genericprocess)
Key Takeaway
Naming conventions aren’t arbitrary—they provide instant visual cues about what you’re dealing with (class vs. method vs. variable). Consistent naming makes code self-documenting.
Exercise 4.4: Dictionary and Variables
Before
camera={'x':0,'y':2.0,'z':1.5}
max_distance=100
default_threshold=0.5
After
camera = {'x': 0, 'y': 2.0, 'z': 1.5}
MAX_DISTANCE = 100
DEFAULT_THRESHOLD = 0.5
Questions
- List all differences between Before and After
- Why are the variable names changed to uppercase in After?
- When should you use UPPER_CASE vs. snake_case for variables?
💡 Show Answer
Differences
- Spaces added around
=assignment operator - Spaces added after colons in dictionary:
'x':0→'x': 0 - Variable names changed to UPPER_CASE for constants:
max_distance→MAX_DISTANCE
Why UPPER_CASE?
The After version assumes these are module-level constants that shouldn’t change during program execution. UPPER_CASE signals to other developers: “Don’t modify this value.”
This is a semantic improvement, not just stylistic:
- Intent is clear: These are configuration values
- Protection from bugs: Accidentally reassigning a constant is more obvious
- Convention alignment: Follows Python and broader programming community standards
When to Use Each Naming Convention
UPPER_CASE (Constants):
- Module-level constants:
PI = 3.14159 - Configuration values:
MAX_SIZE = 1000,CONFIG_PATH = '/etc/app/config.yml' - Magic numbers that never change:
SECONDS_PER_DAY = 86400
snake_case (Variables):
- Regular variables that can change:
camera_x,user_input,temp_result - Function parameters:
def process(data, threshold=10) - Local variables in functions/methods
Special Case - The camera Variable:
The camera dictionary might or might not be a constant depending on context:
# If it's configuration that never changes:
CAMERA_DEFAULT_POSITION = {'x': 0, 'y': 2.0, 'z': 1.5}
# If it's a variable that gets updated:
camera_position = {'x': 0, 'y': 2.0, 'z': 1.5}
# Later in code:
camera_position['x'] = user_input.x
Key Takeaway
Naming conventions communicate intent and mutability. UPPER_CASE tells readers “this won’t change” while snake_case says “this can be reassigned.” Choose wisely based on how the variable will be used.
Exercise 4.5: Complex Function
Before
def process(data,threshold=10,debug=False):
result=[]
for item in data:
if item>threshold:result.append(item*2)
if debug:print(f"Processed {len(result)} items")
return result
After
def process(data, threshold=10, debug=False):
result = []
for item in data:
if item > threshold:
result.append(item * 2)
if debug:
print(f"Processed {len(result)} items")
return result
Questions
- List all differences between Before and After
- Explain why breaking up the compound statements improves readability
- What other improvements would you suggest?
💡 Show Answer
Differences
- Spaces after commas in parameters:
data,threshold→data, threshold - Spaces around
=assignment:result=[]→result = [] - Spaces around
>comparison:item>threshold→item > threshold - Compound statement split:
if item>threshold:result.append(item*2)→ two lines - Compound statement split:
if debug:print(...)→ two lines - Spaces around
*operator:item*2→item * 2
Why Breaking Compound Statements Helps
Readability:
- Each logical operation is on its own line
- Control flow is immediately clear
- Less cognitive load to parse what’s happening
Debugging:
- Can set breakpoints on specific lines
- Stack traces show exact line of failure
- Easier to add print statements or logging
Maintainability:
- Easy to add additional conditions:
if item > threshold and item < max_value: - Easy to expand logic: add logging, error handling, etc.
- Easier to review in version control (changes are line-based)
Example of Easy Extension:
# Before - hard to extend:
if item>threshold:result.append(item*2)
# After - easy to add logging:
if item > threshold:
logger.debug(f"Processing item: {item}")
result.append(item * 2)
Additional Improvements
from typing import List
def process(
data: List[float],
threshold: float = 10,
debug: bool = False
) -> List[float]:
"""
Process data by filtering and doubling values above threshold.
Args:
data: List of numerical values to process
threshold: Minimum value to include (default: 10)
debug: Whether to print debug information (default: False)
Returns:
List of processed values (doubled items above threshold)
Example:
>>> process([5, 15, 20], threshold=10)
[30, 40]
"""
result = []
for item in data:
if item > threshold:
result.append(item * 2)
if debug:
print(f"Processed {len(result)} items")
return result
Further improvements:
- Type hints for better IDE support and type checking
- Docstring with Args, Returns, and Example
- Consider using list comprehension for more Pythonic code:
result = [item * 2 for item in data if item > threshold]
Key Takeaway
Compound statements save vertical space but sacrifice readability, debuggability, and maintainability. The extra lines are worth it. Modern editors collapse code anyway, so vertical space isn’t a real concern.
Exercise 4.6: Real-World Example
Before
from dash import Dash,html,dcc
import plotly.graph_objects as go
import numpy as np
import sys
def create_visualization(x_data,y_data,title="Plot"):
fig=go.Figure()
fig.add_trace(go.Scatter(x=x_data,y=y_data,mode='lines',name='Data'))
fig.update_layout(title=title,xaxis_title="X",yaxis_title="Y")
return fig
APP_title="Road Profile Viewer"
After
from dash import Dash, dcc, html
import numpy as np
import plotly.graph_objects as go
def create_visualization(x_data, y_data, title="Plot"):
fig = go.Figure()
fig.add_trace(go.Scatter(
x=x_data,
y=y_data,
mode='lines',
name='Data'
))
fig.update_layout(
title=title,
xaxis_title="X",
yaxis_title="Y"
)
return fig
APP_TITLE = "Road Profile Viewer"
Questions
- List ALL differences (aim for 10+)
- Explain the import organization changes
- Why is the multi-line function call formatting better?
💡 Show Answer
Differences
- Spaces added after commas in imports:
Dash,html,dcc→Dash, dcc, html - Imports alphabetically sorted within dash import:
html,dcc→dcc, html sysimport removed (unused import)- Imports reordered: dash, numpy, plotly (library organization)
- Spaces after commas in function parameters:
x_data,y_data→x_data, y_data - Spaces around
=assignment forfig:fig=→fig = - Multi-line formatting for
add_trace()call - Multi-line formatting for
update_layout()call - Each parameter on its own line in function calls
- Constant changed from
APP_titletoAPP_TITLE(consistent UPPER_CASE) - Blank line separation between import groups
Import Organization Explained
Before:
from dash import Dash,html,dcc # Mixed order, no spaces
import plotly.graph_objects as go
import numpy as np
import sys # Unused
After:
from dash import Dash, dcc, html # Alphabetical, with spaces
import numpy as np # Alphabetical within group
import plotly.graph_objects as go
# Note: In larger projects, you might separate further:
# 1. Standard library (none here)
# 2. Third-party (dash, numpy, plotly)
# 3. Local application imports
Benefits:
- Alphabetical sorting makes imports easy to find
- Removing unused imports reduces dependencies and import time
- Consistent spacing improves readability
- Logical grouping shows dependency relationships
Why Multi-Line Function Calls Are Better
Comparison:
# Before - everything crammed on one line:
fig.add_trace(go.Scatter(x=x_data,y=y_data,mode='lines',name='Data'))
Problems:
- Hard to read with many parameters
- Easy to miss a parameter
- Difficult to modify
- Poor git diff visibility
# After - one parameter per line:
fig.add_trace(go.Scatter(
x=x_data,
y=y_data,
mode='lines',
name='Data'
))
Advantages:
- Each argument is clearly visible
- Easy to add/remove/modify single argument
- Easy to see what parameters are being set
- Easier to review in version control (one parameter per line)
- More readable for complex function calls
- Self-documenting (parameter names visible without scrolling)
Impact on Version Control (Git Diffs)
Before - Hard to see what changed:
- fig.add_trace(go.Scatter(x=x_data,y=y_data,mode='lines',name='Data'))
+ fig.add_trace(go.Scatter(x=x_data,y=y_data,mode='markers',name='Data'))
What changed? You have to read the entire line carefully.
After - Crystal clear:
fig.add_trace(go.Scatter(
x=x_data,
y=y_data,
- mode='lines',
+ mode='markers',
name='Data'
))
Immediately obvious: mode changed from ‘lines’ to ‘markers’.
When to Use Multi-Line Format
Use multi-line when:
- Function has 3+ parameters
- Line would exceed 88 characters
- Parameters are complex (nested structures, long strings)
- Readability would improve
- Working in team environment (better for code review)
Single line is OK when:
- Function has 1-2 simple parameters
- Total line length < 60 characters
- Parameters are self-explanatory
- Example:
print(result),len(data)
Production-Ready Version
"""Road profile visualization module."""
from typing import Optional
from dash import Dash, dcc, html
import numpy as np
import plotly.graph_objects as go
# Application constants
APP_TITLE = "Road Profile Viewer"
DEFAULT_LINE_COLOR = "blue"
DEFAULT_LINE_WIDTH = 2
def create_visualization(
x_data: np.ndarray,
y_data: np.ndarray,
title: str = "Plot",
line_color: str = DEFAULT_LINE_COLOR,
line_width: int = DEFAULT_LINE_WIDTH
) -> go.Figure:
"""
Create an interactive visualization of road profile data.
Args:
x_data: X-axis coordinates (horizontal distance)
y_data: Y-axis coordinates (elevation or profile height)
title: Plot title (default: "Plot")
line_color: Color of the line trace (default: blue)
line_width: Width of the line trace (default: 2)
Returns:
Plotly Figure object ready for display
Example:
>>> x = np.linspace(0, 100, 50)
>>> y = np.sin(x / 10)
>>> fig = create_visualization(x, y, title="Sine Wave Profile")
"""
fig = go.Figure()
fig.add_trace(go.Scatter(
x=x_data,
y=y_data,
mode='lines',
name='Road Profile',
line=dict(color=line_color, width=line_width)
))
fig.update_layout(
title=title,
xaxis_title="Distance (m)",
yaxis_title="Elevation (m)",
hovermode='x unified',
template='plotly_white'
)
return fig
Key Takeaway
Real-world code complexity demands PEP 8 compliance even more. Multi-line formatting, proper imports, and consistent naming aren’t just style—they’re essential for team collaboration, code review, and long-term maintenance. The time saved in comprehension far outweighs the extra keystrokes.
Learning Assessment
After completing these exercises, you should be able to:
- ✅ Quickly identify differences between compliant and non-compliant code
- ✅ Articulate which PEP 8 rules are violated and why they matter
- ✅ Recognize when code needs refactoring beyond basic PEP 8 compliance
- ✅ Understand the practical benefits of style guidelines (debugging, maintenance, collaboration)
- ✅ Apply these principles when reviewing pull requests or refactoring code
Self-Assessment Quiz
Question 1: Why is def calc(a,b):return a+b problematic even though it “works”?
Answer
Multiple issues:
- Hard to debug - can’t set breakpoint on return statement
- Hard to extend - no room to add validation, logging, etc.
- Violates PEP 8 - compound statement, missing spaces
- Poor readability - requires parsing dense syntax
- Bad naming -
calcis vague
Bottom line: “Working” isn’t enough. Professional code must be readable, maintainable, and follow standards.
Question 2: What’s the #1 benefit of proper import organization?
Answer
Dependency clarity.
When imports are properly grouped and sorted, you can instantly see:
- What external packages are required (deployment concern)
- What’s from standard library vs. third-party (reliability concern)
- What can be removed without breaking code (maintenance concern)
In a project with 50+ imports, this organization saves hours of debugging and dependency management.
Question 3: When should you use multi-line function calls?
Answer
Use multi-line formatting when:
- Function has 3+ parameters
- Line exceeds 88 characters
- Working in a team environment (better for code review)
- Parameters are complex (nested structures, calculations)
- You want self-documenting code
Rule of thumb: If you hesitate, use multi-line. The extra vertical space is worth the clarity.
Next Steps
You’ve Mastered Manual PEP 8 Review! 🎉
Now it’s time to leverage automation:
- Install Ruff in your projects:
uv add ruff --dev - Run automatic checks:
ruff check . ruff format . -
Configure your IDE to show PEP 8 issues in real-time
-
Set up pre-commit hooks to catch issues before commits
- Add CI/CD checks to enforce style in pull requests
Remember the Principle
Manual understanding → Automated enforcement
You’ve developed the eye to spot issues. Now let tools do the repetitive work while you focus on higher-level code quality concerns like:
- Architecture and design patterns
- Performance optimization
- Security vulnerabilities
- Business logic correctness
Continuous Improvement
- Review your own old code with fresh eyes
- Participate in code reviews and share PEP 8 knowledge
- Mentor others - teaching solidifies understanding
- Stay updated - PEP 8 evolves (though slowly)
Additional Resources
- PEP 8 Official: python.org/dev/peps/pep-0008
- Ruff Documentation: docs.astral.sh/ruff
- Black (Code Formatter): black.readthedocs.io
- Real Python PEP 8 Guide: realpython.com/python-pep8
Feedback and Questions
If you found these exercises helpful or have suggestions for improvement, please reach out to your instructor or submit feedback through the course platform.
Happy coding! 🐍✨