Python Linting and Formatting Practice Problems & Exercises
Practice: Linting and Formatting
← Back to lessonEasy
Task: Identify all five violations and rewrite the corrected version. Name each violation type (E-code or W-code).
Violations:
x=5— E225: missing whitespace around operatordef add(a,b):— E231: missing whitespace after commareturn a + b— E111: indentation not a multiple of 4result = add(x,y)— E231: missing whitespace after commaif result == None:— E711: comparison to None should beis None
Solution:
x = 5
y = 10
def add(a, b):
return a + b
result = add(x, y)
if result is None:
print("none")
# Run: flake8 myfile.py → no issues
# Or: ruff check myfile.py
print("5 violations identified and fixed")
# This code has 5 PEP 8 violations. Identify and fix them all.
x=5
y = 10
def add(a,b):
return a + b
result = add(x,y)
if result == None:
print("none")
Expected Output
5 violations identified and fixedHints
Hint 1: E302: expected 2 blank lines before a top-level function or class definition.
Hint 2: E501: line too long — PEP 8 limit is 79 chars (projects often use 88 or 120).
Hint 3: E711: comparison to None should use `is` or `is not`, not `==`.
Hint 4: W291: trailing whitespace on a line.
Task: Manually apply what black would do to this code. Key rules: 4-space indent, spaces around operators, double quotes, trailing comma on multi-line collections, 88-char line limit.
Solution:
# After black formatting
def calculate(x, y, z):
result = x * y + z
return result
data = {"name": "Alice", "age": 30, "city": "London"}
numbers = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
]
# Changes black made:
# - Removed extra space in "def calculate"
# - Added spaces after commas in function signature
# - Fixed indentation (2 spaces → 4)
# - Added spaces around = in result=
# - Changed single quotes to double quotes in dict
# - Wrapped the long list to stay within 88 chars
# - Added trailing comma after last list element
print("black reformatted: 1 file changed")
# Before black formatting — identify what black would change
def calculate(x,y,z):
result=x*y + z
return result
data={'name':'Alice','age':30,'city':'London'}
numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]
Expected Output
black reformatted: 1 file changedHints
Hint 1: `black myfile.py` formats in place. `black --check myfile.py` reports without changing.
Hint 2: black enforces consistent double quotes, trailing commas, and line length of 88.
Hint 3: black is opinionated — there is no configuration for style choices.
Task: Re-order the imports into isort's canonical form: stdlib first, then third-party, then local — each group alphabetically sorted and separated by a blank line.
Solution:
# After isort
# 1. Standard library (alphabetical)
import json
import os
import sys
from collections import defaultdict
# 2. Third-party (alphabetical)
import numpy as np
import requests
# 3. Local application (alphabetical)
from myapp.models import User
from myapp.utils import helper
print("isort fixed 1 file")
# Imports are out of order. Fix them to match isort output.
import requests
import os
from myapp.utils import helper
import sys
import numpy as np
from collections import defaultdict
from myapp.models import User
import json
Expected Output
isort fixed 1 fileHints
Hint 1: isort groups imports into: stdlib, third-party, local — separated by blank lines.
Hint 2: Within each group, imports are sorted alphabetically.
Hint 3: Run: `isort myfile.py` or `isort --check-only myfile.py`.
Task: Fill in the errors dict with a plain-English explanation of each error code.
Solution:
errors = {
"F401": "The module 'os' was imported but never used anywhere in the file — safe to remove.",
"E501": "Line 7 is 92 characters long, which exceeds the 79-char PEP 8 limit.",
"F821": "'confg' is used but was never defined — likely a typo for 'config'.",
"F811": "'process' is defined again on line 15 but the version from line 10 was never used — probably a copy-paste mistake.",
"C901": "The function 'calculate' has a cyclomatic complexity of 11 (too many branches) — consider refactoring.",
}
for code, explanation in errors.items():
print(f"{code}: {explanation}")
print("All 5 error codes explained correctly")
# Given this flake8 output, explain each error in plain English:
#
# app.py:3:1: F401 'os' imported but unused
# app.py:7:5: E501 line too long (92 > 79 characters)
# app.py:12:1: F821 undefined name 'confg'
# app.py:15:1: F811 redefinition of unused 'process' from line 10
# app.py:20:1: C901 'calculate' is too complex (11)
errors = {
"F401": None,
"E501": None,
"F821": None,
"F811": None,
"C901": None,
}
for code, explanation in errors.items():
print(f"{code}: {explanation}")
Expected Output
All 5 error codes explained correctlyHints
Hint 1: E-codes are PEP 8 style errors; W-codes are warnings; F-codes are from pyflakes (undefined/unused names).
Hint 2: F401: imported but unused. F811: redefinition of unused name. F821: undefined name.
Hint 3: C-codes come from plugins like mccabe (cyclomatic complexity).
Medium
Task: Fill in the config string with the correct setup.cfg flake8 configuration.
Solution:
config = """
[flake8]
max-line-length = 88
extend-ignore = E203
per-file-ignores =
tests/*:F401
tests/*:F811
exclude =
.git,
__pycache__,
.tox,
venv
max-complexity = 10
"""
print(config.strip())
print("Configuration written — flake8 would pass with these settings")
# Usage:
# flake8 . → uses this config automatically
# flake8 --config setup.cfg → explicit
# Write the setup.cfg [flake8] section that:
# 1. Sets max line length to 88
# 2. Ignores E203 (whitespace before ':') project-wide — black causes this
# 3. Ignores F401 (unused imports) in all files under tests/
# 4. Excludes the .git and __pycache__ directories
# 5. Sets max complexity to 10
config = """
[flake8]
# fill in here
"""
print(config)
print("Configuration written — flake8 would pass with these settings")
Expected Output
Configuration written — flake8 would pass with these settingsHints
Hint 1: Create a `[flake8]` section in `setup.cfg` or a `.flake8` file.
Hint 2: `max-line-length = 88` matches black's default.
Hint 3: `per-file-ignores = tests/*:F401` ignores unused imports only in test files.
Task: Rewrite the code to fix the pylint issues: meaningful names, docstrings, proper class naming, consistent spacing. The logic must remain identical.
Issues: C0103 (invalid names: account, n, b, d, w, a, x), C0114/C0115/C0116 (missing docstrings), W0612 (variable used only once).
Solution:
"""Module demonstrating a bank account with deposit and withdrawal."""
class Account:
"""A simple bank account with deposit and withdrawal."""
def __init__(self, owner: str, balance: float = 0.0) -> None:
"""Initialise account with owner name and starting balance."""
self.owner = owner
self.balance = balance
def deposit(self, amount: float) -> None:
"""Add amount to the account balance."""
self.balance += amount
def withdraw(self, amount: float) -> None:
"""Subtract amount from balance if sufficient funds exist."""
if amount > self.balance:
print("Insufficient funds")
else:
self.balance -= amount
account = Account("alice", 100)
account.deposit(50)
account.withdraw(30)
print(account.balance) # 120.0
print("pylint score improved: 4.50/10 -> 9.20/10")
# Improve this code's pylint score from ~4/10 to 9+/10
class account:
def __init__(self,n,b):
self.n=n
self.b=b
def d(self,x):
self.b=self.b+x
def w(self,x):
if x>self.b:
print("nope")
else:
self.b=self.b-x
a=account("alice",100)
a.d(50)
a.w(30)
print(a.b)
Expected Output
pylint score improved: 4.50/10 -> 9.20/10Hints
Hint 1: pylint scores from 0–10. Below 7 is poor; above 9 is good. 10 is rare.
Hint 2: C-codes: convention; R-codes: refactoring suggestions; W-codes: warnings; E-codes: errors.
Hint 3: Add `# pylint: disable=C0114` on a line or at file top to suppress specific messages.
Task: Fill in the ruff_config string with a correct pyproject.toml ruff configuration.
Solution:
ruff_config = """
[tool.ruff]
target-version = "py311"
line-length = 88
exclude = [
"migrations/",
".git",
"__pycache__",
"venv",
]
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
ignore = ["E501"]
[tool.ruff.lint.isort]
known-first-party = ["myapp"]
"""
print(ruff_config.strip())
print("ruff check: 0 errors")
print("ruff format: 0 files reformatted")
# Common ruff commands:
# ruff check . -- lint all files
# ruff check --fix . -- lint and auto-fix
# ruff format . -- format (replaces black)
# ruff check --select I . -- isort checks only
# Write a pyproject.toml [tool.ruff] configuration that:
# 1. Targets Python 3.11+
# 2. Sets line length to 88
# 3. Enables rules: E (pycodestyle), F (pyflakes), I (isort), UP (pyupgrade)
# 4. Ignores E501 (line too long — handled by formatter)
# 5. Excludes migrations/ directory
ruff_config = """
[tool.ruff]
# fill in here
[tool.ruff.lint]
# fill in here
"""
print(ruff_config)
print("ruff check: 0 errors")
print("ruff format: 0 files reformatted")
Expected Output
ruff check: 0 errors\nruff format: 0 files reformattedHints
Hint 1: ruff replaces flake8, isort, and many pylint checks in one fast tool.
Hint 2: Configure in `pyproject.toml` under `[tool.ruff]` and `[tool.ruff.lint]`.
Hint 3: `ruff check --fix .` auto-fixes many issues; `ruff format .` replaces black.
Task: Fix all three type errors. Use Optional[int] for nullable returns, correct the return type annotation, and guard against None before string concatenation.
Solution:
from typing import Optional, List
# Fix 1: Return type is Optional[int] since None is a valid return
def get_first(items: List[int]) -> Optional[int]:
if not items:
return None
return items[0]
# Fix 2: Return float (2.0 is float) or use integer multiplication
def double(x: int) -> float:
return x * 2.0
# Alternative: keep return type int and use integer multiply
def double_int(x: int) -> int:
return x * 2
# Fix 3: Guard against None before concatenation
def greet(name: Optional[str]) -> str:
if name is None:
return "Hello, stranger"
return "Hello, " + name
print("mypy: 3 errors found and fixed")
# Run: mypy --strict thisfile.py → Success: no issues found
from typing import Optional, List
def get_first(items: List[int]) -> int:
if not items:
return None # Error: None is incompatible with int return type
return items[0]
def double(x: int) -> int:
return x * 2.0 # Error: float incompatible with int
def greet(name: Optional[str]) -> str:
return "Hello, " + name # Error: name could be None
# Fix all 3 type errors
Expected Output
mypy: 3 errors found and fixedHints
Hint 1: mypy performs static type checking — it catches type errors without running code.
Hint 2: Run: `mypy myfile.py` or `mypy --strict myfile.py` for stricter checks.
Hint 3: Common errors: Incompatible return type, Argument 1 has incompatible type, Item "None" has no attribute.
Hard
Task: Fill in all three Makefile targets with the correct tool commands. The check target must be safe for CI (exit non-zero on failure, no file modifications).
Solution:
makefile_content = """
.PHONY: lint format check
# Read-only analysis: report errors but don't modify files
lint:
\truff check .
\tmypy src --strict
# Modify files in place: fix import order and formatting
format:
\tisort .
\tblack .
\truff check --fix .
# CI-safe: exit 1 if anything would change, no modifications
check:
\tisort --check-only --diff .
\tblack --check --diff .
\truff check .
\tmypy src --strict
"""
print(makefile_content.strip())
print("Linting pipeline defined: isort + black + ruff + mypy")
# CI pipeline usage (.gitlab-ci.yml):
# lint:
# script:
# - pip install ruff black isort mypy
# - make check
# Write a Makefile with three targets:
# 1. 'lint' — run ruff check and mypy (read-only checks)
# 2. 'format' — run isort and black (modify files)
# 3. 'check' — run isort --check, black --check, ruff check (CI-safe, no modifications)
makefile_content = """
.PHONY: lint format check
lint:
# fill in
format:
# fill in
check:
# fill in
"""
print(makefile_content)
print("Linting pipeline defined: isort + black + ruff + mypy")
Expected Output
Linting pipeline defined: isort + black + ruff + mypyHints
Hint 1: A linting pipeline runs tools in order: isort → black → ruff → mypy.
Hint 2: Use a `Makefile` target or a shell script to chain them.
Hint 3: In CI, `--check` / `--diff` flags make tools exit with non-zero on failure without modifying files.
Task: Implement NoPrintChecker.run to walk the AST and yield a violation tuple for each print(...) call in the source.
Solution:
import ast
SOURCE = """
x = 1
print("debug value:", x)
import logging
logger = logging.getLogger(__name__)
logger.info("proper log")
print("another debug")
"""
class NoPrintChecker:
name = "no-print"
version = "0.1.0"
def __init__(self, tree):
self.tree = tree
def run(self):
for node in ast.walk(self.tree):
if isinstance(node, ast.Call):
# Handle bare print() and print via attribute access
func = node.func
if isinstance(func, ast.Name) and func.id == "print":
yield (
node.lineno,
node.col_offset,
"EOA001 use logging instead of print",
type(self),
)
tree = ast.parse(SOURCE)
checker = NoPrintChecker(tree)
violations = list(checker.run())
print(f"Custom rule: EOA001 detected {len(violations)} violations")
for line, col, msg, _ in violations:
print(f"line {line}: print used")
import ast
# Simulate a custom flake8 plugin that flags bare 'print' calls
# Rule: EOA001 — use logging instead of print statements
SOURCE = """
x = 1
print("debug value:", x)
import logging
logger = logging.getLogger(__name__)
logger.info("proper log")
print("another debug")
"""
class NoPrintChecker:
name = "no-print"
version = "0.1.0"
def __init__(self, tree):
self.tree = tree
def run(self):
# Walk the AST looking for Call nodes where func.id == 'print'
# yield (line_number, col_offset, "EOA001 use logging instead of print", type(self))
pass
# Parse and run the checker
tree = ast.parse(SOURCE)
checker = NoPrintChecker(tree)
violations = list(checker.run())
print(f"Custom rule: EOA001 detected {len(violations)} violations")
for line, col, msg, _ in violations:
print(f"line {line}: {msg.split()[0]} used")
Expected Output
Custom rule: EOA001 detected 2 violations\n['line 3: print used', 'line 7: print used']Hints
Hint 1: flake8 plugins use Python AST (Abstract Syntax Tree) visitors to inspect code.
Hint 2: A plugin is a class with `__init__(self, tree)` and `run(self)` that yields tuples of `(line, col, message, type)`.
Hint 3: Register via entry points in `setup.cfg` under `[options.entry_points] flake8.extension`.
Task: Fill in the three pyproject.toml content strings with appropriate ruff configuration for the monorepo scenario.
Solution:
shared = """
[tool.ruff]
line-length = 88
exclude = [".git", "__pycache__", "venv", "*.egg-info"]
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
"""
core_override = """
# packages/core/pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
# Core package enables pyupgrade in addition to shared rules
select = ["E", "F", "I", "UP"]
ignore = []
"""
api_override = """
# packages/api/pyproject.toml
[tool.ruff]
line-length = 88
[tool.ruff.lint]
# API package adds bugbear, ignores E501 for long route strings
select = ["E", "F", "I", "B"]
ignore = ["E501"]
"""
print("SHARED:", shared.strip())
print()
print("CORE:", core_override.strip())
print()
print("API:", api_override.strip())
print()
print("Monorepo lint strategy defined: shared + per-package overrides")
# Makefile target to lint all packages:
# lint-all:
# for pkg in packages/*/; do ruff check $$pkg; done
# Monorepo structure:
# /
# ├── pyproject.toml <- shared lint config
# ├── packages/
# │ ├── core/
# │ │ └── pyproject.toml <- core overrides
# │ └── api/
# │ └── pyproject.toml <- api overrides
# Write the content of each pyproject.toml file.
# Rules:
# - Shared: ruff, line-length=88, select E+F+I
# - core/ override: also enable UP (pyupgrade), min Python 3.11
# - api/ override: ignore E501 (long URLs in route strings), add B (bugbear)
shared = """[tool.ruff]
# fill in
"""
core_override = """[tool.ruff]
# fill in
"""
api_override = """[tool.ruff]
# fill in
"""
print("SHARED:", shared)
print("CORE:", core_override)
print("API:", api_override)
print("Monorepo lint strategy defined: shared + per-package overrides")
Expected Output
Monorepo lint strategy defined: shared + per-package overridesHints
Hint 1: In a monorepo, a root `pyproject.toml` or `.flake8` provides shared defaults.
Hint 2: Individual packages can override specific rules in their own `pyproject.toml`.
Hint 3: tox or a Makefile recursion strategy can run linting across all packages.
