Skip to main content

Python Linting and Formatting Practice Problems & Exercises

Practice: Linting and Formatting

11 problems4 Easy4 Medium3 Hard45–60 min
← Back to lesson

Easy

#1Spot the PEP 8 ViolationsEasy
PEP8flake8stylewhitespace

Task: Identify all five violations and rewrite the corrected version. Name each violation type (E-code or W-code).

Violations:

  1. x=5 — E225: missing whitespace around operator
  2. def add(a,b): — E231: missing whitespace after comma
  3. return a + b — E111: indentation not a multiple of 4
  4. result = add(x,y) — E231: missing whitespace after comma
  5. if result == None: — E711: comparison to None should be is 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 fixed
Hints

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.


#2black Formatting — Before and AfterEasy
blackformattingauto-format

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 changed
Hints

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.


#3isort — Organising Import OrderEasy
isortimportsorganisation

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 file
Hints

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`.


#4Interpreting a flake8 Error ReportEasy
flake8error-codesF-codesE-codes

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 correctly
Hints

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

#5Configuring flake8 via setup.cfgMedium
flake8configurationsetup.cfgper-file-ignores

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 settings
Hints

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.


#6pylint — Understanding the ScoreMedium
pylintscoremessagesconventions

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/10
Hints

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.


#7ruff — The Modern LinterMedium
rufflintingconfigurationpyproject.toml

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 reformatted
Hints

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.


#8Type Checking with mypyMedium
mypytype-checkingtype-hintsstatic-analysis

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 fixed
Hints

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

#9Building a Full Linting PipelineHard
lintingpipelineMakefileautomation

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 + mypy
Hints

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.


#10Custom flake8 Plugin ConceptHard
flake8pluginASTcustom-rule

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`.


#11Enforcing Standards Across a Multi-Package MonorepoHard
lintingmonorepopyproject.tomltoxmulti-package

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 overrides
Hints

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.

© 2026 EngineersOfAI. All rights reserved.