Home

02 Exercise: PEP 8 - Level 4: Before/After Comparison

exercises chapter-02 pep8 code-quality python practice comparison

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:

Instructions:

  1. Study both the “Before” and “After” versions carefully
  2. Identify ALL differences between the two versions
  3. Answer the questions about which PEP 8 rules are being applied
  4. Consider what additional improvements could be made
  5. 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

  1. List all differences between Before and After
  2. Which specific PEP 8 rules make the After version better?
  3. Are there any additional improvements you would suggest?
💡 Show Answer

Differences

  1. Space added after comma in parameters: a,ba, b
  2. Function body moved to new line instead of same line as def
  3. Spaces added around + operator: a+ba + b

PEP 8 Rules Applied

  1. Use spaces after commas in parameter lists
  2. Avoid multiple statements on one line (compound statements discouraged)
  3. Surround binary operators with spaces

Additional Improvements

  • Function name calc is vague; better: calculate_sum or add_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

  1. List all differences between Before and After
  2. Which specific PEP 8 rules make the After version better?
  3. Why is the blank line in the After version important?
💡 Show Answer

Differences

  1. Each import on separate line
  2. Standard library imports alphabetically sorted (json, os, sys)
  3. Spaces added after commas in from import: List,DictDict, List
  4. Typing imports alphabetically sorted: List,DictDict, List
  5. Blank line separating standard library from third-party imports
  6. Imports grouped by type (standard library, then third-party)

PEP 8 Rules Applied

  1. Imports should usually be on separate lines
  2. Imports should be grouped: standard library, related third-party, local application/library
  3. Imports within each group should be alphabetically sorted
  4. Use spaces after commas
  5. 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

  1. List all differences between Before and After
  2. Which specific PEP 8 rules make the After version better?
  3. What additional improvements would make this code production-ready?
💡 Show Answer

Differences

  1. Class name changed from snake_case to PascalCase: dataProcessorDataProcessor
  2. Space added after comma in __init__ parameters: self,itemsself, items
  3. Spaces added around = in assignments: self.items=itemsself.items = items
  4. Method name changed from PascalCase to snake_case: Processprocess
  5. Method body moved to separate line (no compound statement)
  6. Blank line added between methods
  7. Spaces added around * operator in list comprehension: x*2x * 2

PEP 8 Rules Applied

  1. Class names should use PascalCase (CapWords convention)
  2. Method names should use snake_case (lowercase with underscores)
  3. Spaces after commas
  4. Spaces around operators (assignment and arithmetic)
  5. Avoid compound statements (multiple statements on one line)
  6. 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_values instead of generic process)

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

  1. List all differences between Before and After
  2. Why are the variable names changed to uppercase in After?
  3. When should you use UPPER_CASE vs. snake_case for variables?
💡 Show Answer

Differences

  1. Spaces added around = assignment operator
  2. Spaces added after colons in dictionary: 'x':0'x': 0
  3. Variable names changed to UPPER_CASE for constants: max_distanceMAX_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

  1. List all differences between Before and After
  2. Explain why breaking up the compound statements improves readability
  3. What other improvements would you suggest?
💡 Show Answer

Differences

  1. Spaces after commas in parameters: data,thresholddata, threshold
  2. Spaces around = assignment: result=[]result = []
  3. Spaces around > comparison: item>thresholditem > threshold
  4. Compound statement split: if item>threshold:result.append(item*2) → two lines
  5. Compound statement split: if debug:print(...) → two lines
  6. Spaces around * operator: item*2item * 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

  1. List ALL differences (aim for 10+)
  2. Explain the import organization changes
  3. Why is the multi-line function call formatting better?
💡 Show Answer

Differences

  1. Spaces added after commas in imports: Dash,html,dccDash, dcc, html
  2. Imports alphabetically sorted within dash import: html,dccdcc, html
  3. sys import removed (unused import)
  4. Imports reordered: dash, numpy, plotly (library organization)
  5. Spaces after commas in function parameters: x_data,y_datax_data, y_data
  6. Spaces around = assignment for fig: fig=fig =
  7. Multi-line formatting for add_trace() call
  8. Multi-line formatting for update_layout() call
  9. Each parameter on its own line in function calls
  10. Constant changed from APP_title to APP_TITLE (consistent UPPER_CASE)
  11. 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:


Self-Assessment Quiz

Question 1: Why is def calc(a,b):return a+b problematic even though it “works”?

Answer

Multiple issues:

  1. Hard to debug - can’t set breakpoint on return statement
  2. Hard to extend - no room to add validation, logging, etc.
  3. Violates PEP 8 - compound statement, missing spaces
  4. Poor readability - requires parsing dense syntax
  5. Bad naming - calc is 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:

  1. Function has 3+ parameters
  2. Line exceeds 88 characters
  3. Working in a team environment (better for code review)
  4. Parameters are complex (nested structures, calculations)
  5. 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:

  1. Install Ruff in your projects:
    uv add ruff --dev
    
  2. Run automatic checks:
    ruff check .
    ruff format .
    
  3. Configure your IDE to show PEP 8 issues in real-time

  4. Set up pre-commit hooks to catch issues before commits

  5. 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:

Continuous Improvement


Additional Resources


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! 🐍✨

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk