Skip to main content

Python Formatting and Tooling Practice Problems & Exercises

Practice: Formatting and Tooling

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Black: Magic Trailing Comma BehaviorEasy
blacktrailing-commaformatting

Predict the output. Two functions are formatted by Black. One has a magic trailing comma; the other does not. Identify which Black format rule applies to each.

Python
# Simulate Black's decision based on trailing comma presence

def check_black_behavior(code_snippet, has_trailing_comma):
    """
    Returns whether Black would keep the call multi-line.
    Black rule: trailing comma -> always multi-line.
    No trailing comma + fits in 88 chars -> may collapse to one line.
    """
    if has_trailing_comma:
        return "always_multiline"
    else:
        return "single_line_ok"

# Case 1: function call with trailing comma after last arg
result_a = check_black_behavior(
    "func(arg_one, arg_two, arg_three,)",
    has_trailing_comma=True,
)
print(result_a)

# Case 2: function call without trailing comma, fits on one line
result_b = check_black_behavior(
    "func(arg_one, arg_two, arg_three)",
    has_trailing_comma=False,
)
print(result_b)
Solution
def check_black_behavior(code_snippet, has_trailing_comma):
if has_trailing_comma:
return "always_multiline"
else:
return "single_line_ok"

result_a = check_black_behavior(
"func(arg_one, arg_two, arg_three,)",
has_trailing_comma=True,
)
print(result_a)

result_b = check_black_behavior(
"func(arg_one, arg_two, arg_three)",
has_trailing_comma=False,
)
print(result_b)

Output:

always_multiline
single_line_ok

How it works: Black's "magic trailing comma" rule is one of its most important behaviors. When Black encounters a trailing comma after the last element of a multi-line structure (function call, list, dict, etc.), it treats that comma as a deliberate signal: keep this multi-line regardless of length. Remove the trailing comma and Black will collapse the structure to one line if the total length fits within the 88-character limit.

Key insight: Use the trailing comma intentionally. Add it when you want a function signature, long import, or configuration dict to always remain expanded — this makes diffs cleaner because adding a new argument only adds one line rather than reformatting the entire call. Remove it when you are fine with Black deciding based on line length.

Expected Output
always_multiline\nsingle_line_ok
Hints

Hint 1: A trailing comma after the last element signals to Black that the structure must always stay multi-line.

Hint 2: Without a trailing comma, Black may collapse the structure to one line if it fits within the line-length limit.

#2Black: Double Quote NormalizationEasy
blackquotesstring-normalization

Predict the output. Black has a rule for string quotes: prefer double, but avoid introducing backslash escapes. Given three strings, determine which quote style Black would choose for each.

Python
def black_quote_style(original: str, contains_double_quote: bool, contains_apostrophe: bool) -> str:
    """
    Simulate Black's quote normalization rule:
    - Prefer double quotes.
    - If the string already contains a double quote character, keep single quotes
      to avoid needing a backslash escape.
    - An apostrophe (single quote inside) does NOT prevent double-quoting.
    """
    if contains_double_quote:
        return "single"
    return "double"

# Plain string — no embedded quotes
plain = black_quote_style("hello world", contains_double_quote=False, contains_apostrophe=False)
print(plain)

# String containing a double quote: She said "hello"
with_double = black_quote_style('She said "hello"', contains_double_quote=True, contains_apostrophe=False)
print(with_double)

# String with apostrophe only: It's working
with_apostrophe = black_quote_style("It's working", contains_double_quote=False, contains_apostrophe=True)
print(with_apostrophe)
Solution
def black_quote_style(original: str, contains_double_quote: bool, contains_apostrophe: bool) -> str:
if contains_double_quote:
return "single"
return "double"

plain = black_quote_style("hello world", contains_double_quote=False, contains_apostrophe=False)
print(plain)

with_double = black_quote_style('She said "hello"', contains_double_quote=True, contains_apostrophe=False)
print(with_double)

with_apostrophe = black_quote_style("It's working", contains_double_quote=False, contains_apostrophe=True)
print(with_apostrophe)

Output:

double
single
double

How it works:

  1. "hello world" — no embedded quotes. Black normalizes to double quotes.
  2. 'She said "hello"' — contains a " character. Converting to double quotes would require \" inside, introducing a backslash. Black leaves it as single quotes.
  3. "It's working" — contains an apostrophe ('), which is a single-quote character. Converting to double quotes does not require any escaping because the inner character is ', not ". Black uses double quotes.

Key insight: Black's rule is: "use double quotes unless the string already contains double quotes." An apostrophe (single quote) inside a string is not a reason to keep single quotes — double-quoting "It's working" is perfectly valid Python. This surprises many developers who assume Black would "prefer whichever quote avoids escaping," but the rule specifically only cares about double quotes.

Expected Output
double\nsingle\nsingle
Hints

Hint 1: Black converts string literals to double quotes by default.

Hint 2: Black leaves a string in single quotes if converting would require adding a backslash escape.

#3isort: Import Group ClassificationEasy
isortimport-groupsstdlibthird-party

Classify imports. For each module name, determine which isort group it belongs to: stdlib, third_party, or first_party.

Python
import sys

STDLIB_MODULES = set(sys.stdlib_module_names)

# Known third-party packages for this exercise
KNOWN_THIRD_PARTY = {"requests", "fastapi", "pydantic", "django", "flask", "sqlalchemy", "numpy", "pandas"}

# Simulated first-party app modules
KNOWN_FIRST_PARTY = {"myapp", "api", "models", "utils", "config"}

def classify_import(module_name: str) -> str:
    root = module_name.split(".")[0]
    if root in KNOWN_FIRST_PARTY:
        return "first_party"
    if root in KNOWN_THIRD_PARTY:
        return "third_party"
    if root in STDLIB_MODULES:
        return "stdlib"
    return "unknown"

print(classify_import("os.path"))
print(classify_import("requests"))
print(classify_import("myapp.models"))
print(classify_import("typing"))
Solution
import sys

STDLIB_MODULES = set(sys.stdlib_module_names)
KNOWN_THIRD_PARTY = {"requests", "fastapi", "pydantic", "django", "flask", "sqlalchemy", "numpy", "pandas"}
KNOWN_FIRST_PARTY = {"myapp", "api", "models", "utils", "config"}

def classify_import(module_name: str) -> str:
root = module_name.split(".")[0]
if root in KNOWN_FIRST_PARTY:
return "first_party"
if root in KNOWN_THIRD_PARTY:
return "third_party"
if root in STDLIB_MODULES:
return "stdlib"
return "unknown"

print(classify_import("os.path"))
print(classify_import("requests"))
print(classify_import("myapp.models"))
print(classify_import("typing"))

Output:

stdlib
third_party
first_party
stdlib

How it works: isort uses the root module name (the part before the first .) to classify imports. os.pathos is stdlib. requests — a third-party package. myapp.modelsmyapp is first-party (your application code, listed in known_first_party). typing — stdlib since Python 3.5.

isort uses sys.stdlib_module_names (Python 3.10+) to identify stdlib modules and pip metadata for third-party packages. When it cannot determine a module's category, it defaults to third-party.

Key insight: The most common isort configuration mistake is not declaring your own application packages in known_first_party. Without this, isort cannot tell whether myapp is a third-party package you installed or your own local code. This causes isort to put from myapp.models import User in the wrong group, resulting in a conflict with Black on every commit.

Expected Output
stdlib\nthird_party\nfirst_party\nstdlib
Hints

Hint 1: isort groups imports into three categories: stdlib (Python built-ins), third-party (installed packages), and first-party (your own application code).

Hint 2: The typing module is part of the Python standard library.

Hint 3: requests, fastapi, pydantic are third-party packages.

#4flake8: Reading Error CodesEasy
flake8error-codesE-codesF-codes

Classify flake8 codes. Determine whether each flake8 error code indicates a potential real bug (bug_risk) or a pure style violation (style_only).

Python
def classify_flake8_code(code: str) -> str:
    """
    F-series: pyflakes — undefined names, unused imports, etc. — real bug risk.
    E-series, W-series: pycodestyle — spacing, line length, blank lines — style only.
    B-series (bugbear): also bug risk.
    """
    prefix = code[0]
    if prefix == "F":
        return "bug_risk"
    if prefix == "B":
        return "bug_risk"
    return "style_only"

codes = [
    "F821",   # undefined name
    "E302",   # expected 2 blank lines
    "F841",   # local variable assigned but never used
    "W503",   # line break before binary operator
    "F401",   # imported but unused
]

for code in codes:
    print(classify_flake8_code(code))
Solution
def classify_flake8_code(code: str) -> str:
prefix = code[0]
if prefix == "F":
return "bug_risk"
if prefix == "B":
return "bug_risk"
return "style_only"

codes = ["F821", "E302", "F841", "W503", "F401"]
for code in codes:
print(classify_flake8_code(code))

Output:

bug_risk
style_only
bug_risk
style_only
bug_risk

How it works:

  • F821 — undefined name. If processs is called but not defined, this will raise NameError at runtime. Real bug.
  • E302 — expected 2 blank lines, found 1. Pure PEP 8 style. The code runs correctly either way.
  • F841 — local variable assigned but never used. Usually indicates a missing return or copy-paste error. Real bug.
  • W503 — line break before binary operator. Style preference only; no runtime impact.
  • F401 — imported but unused. Not a crash, but clutters the namespace and often indicates dead code or a copy-paste error.

Key insight: When configuring flake8 for a team, prioritize the F-series codes — they have the highest signal-to-noise ratio for catching real problems. E and W codes are valuable for style consistency but are safe to tune or ignore if they conflict with Black's output (E203, W503 are the standard ones to ignore when using Black).

Expected Output
bug_risk\nstyle_only\nbug_risk\nstyle_only\nbug_risk
Hints

Hint 1: F-series codes (F401, F811, F821, F841) come from pyflakes and indicate potential real bugs.

Hint 2: E-series and W-series codes come from pycodestyle and indicate style violations only.


Medium

#5isort: Fix Disordered ImportsMedium
isortimport-orderingprofile-black

Verify isort ordering rules. Given a correctly sorted import block, write assertions confirming that each isort rule is applied correctly.

Python
# Simulated "after isort with profile=black" import ordering
# We verify the rules, not the actual tool output

import ast
import json
import os
import sys
from typing import Optional

import requests
from pydantic import BaseModel

from myapp.models import User
from myapp.utils import helpers

# Verification — treat each group as a list of module names
stdlib_group = ["ast", "json", "os", "sys", "typing"]
third_party_group = ["pydantic", "requests"]
first_party_group = ["myapp.models", "myapp.utils"]

# Rule 1: stdlib comes before third-party
print(stdlib_group[0] < third_party_group[0])

# Rule 2: within stdlib group, names are alphabetically sorted
print(stdlib_group == sorted(stdlib_group))

# Rule 3: within third-party group, names are alphabetically sorted
print(third_party_group == sorted(third_party_group))

# Rule 4: first-party comes last
all_groups = [stdlib_group, third_party_group, first_party_group]
print(len(all_groups) == 3)
Solution
stdlib_group = ["ast", "json", "os", "sys", "typing"]
third_party_group = ["pydantic", "requests"]
first_party_group = ["myapp.models", "myapp.utils"]

print(stdlib_group[0] < third_party_group[0])
print(stdlib_group == sorted(stdlib_group))
print(third_party_group == sorted(third_party_group))
all_groups = [stdlib_group, third_party_group, first_party_group]
print(len(all_groups) == 3)

Output:

True
True
True
True

How it works: isort enforces three ordering rules simultaneously: (1) group order — stdlib, then third-party, then first-party, each separated by a blank line; (2) alphabetical order within each group; (3) import X lines before from X import Y lines within the same group (when not using the black profile).

The profile = "black" setting in pyproject.toml is critical. Without it, isort and Black produce conflicting output for multi-name from X import lines. With profile = "black", isort uses Black-compatible wrapping and trailing comma behavior, so both tools agree on the final format and the pre-commit loop terminates on the first pass.

Key insight: The most common failure mode when first adding both Black and isort to a project is seeing files ping-pong between the two formatters — Black reformats, isort changes it back, Black reformats again. The fix is always profile = "black" in the isort config. This is a one-line addition to pyproject.toml that eliminates the entire class of conflicts.

Expected Output
True\nTrue\nTrue\nTrue
Hints

Hint 1: isort expects: stdlib group first, then third-party group, then first-party group, each separated by a blank line.

Hint 2: Within each group, imports are sorted alphabetically.

Hint 3: isort with profile="black" keeps each import on its own line for multi-name from-imports.

#6flake8: Identify F-Series BugsMedium
flake8F401F811F821F841pyflakes

Match the bug to the flake8 code. Four code snippets each have a specific pyflakes issue. Identify the correct F-code for each.

Python
def identify_flake8_issue(description: str) -> str:
    rules = {
        "import exists but name never used in file": "F401",
        "function defined twice, first definition dead": "F811",
        "name referenced that was never defined": "F821",
        "variable assigned but never read or returned": "F841",
    }
    return rules[description]

issues = [
    "import exists but name never used in file",
    "function defined twice, first definition dead",
    "name referenced that was never defined",
    "variable assigned but never read or returned",
]

for issue in issues:
    print(identify_flake8_issue(issue))
Solution
def identify_flake8_issue(description: str) -> str:
rules = {
"import exists but name never used in file": "F401",
"function defined twice, first definition dead": "F811",
"name referenced that was never defined": "F821",
"variable assigned but never read or returned": "F841",
}
return rules[description]

issues = [
"import exists but name never used in file",
"function defined twice, first definition dead",
"name referenced that was never defined",
"variable assigned but never read or returned",
]

for issue in issues:
print(identify_flake8_issue(issue))

Output:

F401
F811
F821
F841

How it works: Each F-code maps to a specific pyflakes analysis:

  • F401 (import X — unused): import json at the top of a file, but json is never used. Often a leftover from deleted code. Clutters the namespace and confuses readers.
  • F811 (redefinition of unused name): Two def validate(data): in the same file. The first is completely overwritten; its code is dead. Almost always a copy-paste error.
  • F821 (undefined name): Calling processs(data) when only process is defined. This will crash with NameError at runtime. The typo is invisible without static analysis.
  • F841 (local variable assigned but never used): result = expensive_computation() inside a function, but result is never returned, printed, or read. Often indicates a missing return result statement.

Key insight: F821 is the highest-severity flake8 code in practice because it represents a guaranteed runtime crash on that code path. If you only fix one category of flake8 warnings, fix F821s first. F841 is the second most valuable because it almost always indicates a missing return statement — a subtle logic bug that does not crash but produces the wrong output.

Expected Output
F401\nF811\nF821\nF841
Hints

Hint 1: F401: an import statement appears but the imported name is never referenced.

Hint 2: F811: a name is defined twice — the first definition is overwritten and never used.

Hint 3: F821: a name is used but was never defined in this scope.

Hint 4: F841: a local variable is assigned but the variable is never read afterward.

#7mypy: Spot the Type ErrorMedium
mypytype-errorsNone-safetyincompatible-types

Identify mypy errors. Four code snippets are analyzed by mypy. Determine whether each would produce a type error.

Python
from typing import Sequence

def would_mypy_flag(scenario: str) -> str:
    errors = {
        # Accessing attribute on a value typed as X | None without None check
        "access_attr_on_optional": "type_error",
        # Passing tuple[int,...] to a parameter typed list[int]
        "pass_tuple_to_list_param": "type_error",
        # Passing tuple[int,...] to a parameter typed Sequence[int]
        "pass_tuple_to_sequence_param": "no_error",
        # Calling a function with str where int is expected
        "pass_str_to_int_param": "type_error",
    }
    return errors[scenario]

scenarios = [
    "access_attr_on_optional",
    "pass_tuple_to_list_param",
    "pass_tuple_to_sequence_param",
    "pass_str_to_int_param",
]

for scenario in scenarios:
    print(would_mypy_flag(scenario))
Solution
def would_mypy_flag(scenario: str) -> str:
errors = {
"access_attr_on_optional": "type_error",
"pass_tuple_to_list_param": "type_error",
"pass_tuple_to_sequence_param": "no_error",
"pass_str_to_int_param": "type_error",
}
return errors[scenario]

scenarios = [
"access_attr_on_optional",
"pass_tuple_to_list_param",
"pass_tuple_to_sequence_param",
"pass_str_to_int_param",
]

for scenario in scenarios:
print(would_mypy_flag(scenario))

Output:

type_error
type_error
no_error
type_error

How it works:

  1. access_attr_on_optional: If find_user() returns User | None, doing find_user().email triggers Item "None" of "User | None" has no attribute "email". You must guard with if user is not None: first.

  2. pass_tuple_to_list_param: A list[int] is mutable and supports .append(). A tuple[int, ...] is immutable. mypy enforces that a tuple is not substitutable for a list because they have incompatible mutation APIs.

  3. pass_tuple_to_sequence_param: Sequence[int] is a read-only abstract type that both list and tuple satisfy. Using Sequence[int] in function signatures is the correct way to accept any ordered collection of ints without caring about mutability.

  4. pass_str_to_int_param: Passing "hello" to a function typed (a: int) -> int is a clear incompatible type error: Argument 1 has incompatible type "str"; expected "int".

Key insight: The most valuable mypy errors in practice are the X | None attribute access errors. Python code is filled with functions that return Optional[Something] — a database lookup, a dict .get(), a list .pop() — and mypy catches every place where you forget to check for None before using the result. In a medium-sized codebase, running mypy --strict for the first time often reveals dozens of potential AttributeError crashes that would only appear at runtime on specific inputs.

Expected Output
type_error\ntype_error\nno_error\ntype_error
Hints

Hint 1: If a function can return None, you must check for None before accessing attributes or indexing.

Hint 2: Passing a tuple where a list is expected is a type error — use Sequence[int] to accept both.

Hint 3: mypy catches these statically before the code runs.

#8pre-commit: Hook Execution FlowMedium
pre-commithooksworkflowgit-commit

Trace the pre-commit workflow. Given the state of each hook run, determine whether the commit is blocked or passed.

Python
def pre_commit_outcome(hook_results: list) -> str:
    """
    hook_results: list of dicts with:
      - 'name': str
      - 'modified_files': bool  (formatter changed a file)
      - 'exit_code': int        (0 = pass, nonzero = error found)

    If any hook modified files OR returned nonzero exit code, commit is blocked.
    """
    for hook in hook_results:
        if hook["modified_files"] or hook["exit_code"] != 0:
            return "blocked"
    return "passed"

# Scenario 1: Black reformatted a file
scenario_1 = [
    {"name": "black", "modified_files": True, "exit_code": 0},
    {"name": "isort", "modified_files": False, "exit_code": 0},
    {"name": "flake8", "modified_files": False, "exit_code": 0},
]
print(pre_commit_outcome(scenario_1))

# Scenario 2: flake8 found an undefined name (F821)
scenario_2 = [
    {"name": "black", "modified_files": False, "exit_code": 0},
    {"name": "isort", "modified_files": False, "exit_code": 0},
    {"name": "flake8", "modified_files": False, "exit_code": 1},
]
print(pre_commit_outcome(scenario_2))

# Scenario 3: All hooks pass, no modifications
scenario_3 = [
    {"name": "black", "modified_files": False, "exit_code": 0},
    {"name": "isort", "modified_files": False, "exit_code": 0},
    {"name": "flake8", "modified_files": False, "exit_code": 0},
    {"name": "mypy", "modified_files": False, "exit_code": 0},
]
print(pre_commit_outcome(scenario_3))

# Scenario 4: Only trailing-whitespace hook runs, passes
scenario_4 = [
    {"name": "trailing-whitespace", "modified_files": False, "exit_code": 0},
]
print(pre_commit_outcome(scenario_4))
Solution
def pre_commit_outcome(hook_results: list) -> str:
for hook in hook_results:
if hook["modified_files"] or hook["exit_code"] != 0:
return "blocked"
return "passed"

scenario_1 = [
{"name": "black", "modified_files": True, "exit_code": 0},
{"name": "isort", "modified_files": False, "exit_code": 0},
{"name": "flake8", "modified_files": False, "exit_code": 0},
]
print(pre_commit_outcome(scenario_1))

scenario_2 = [
{"name": "black", "modified_files": False, "exit_code": 0},
{"name": "isort", "modified_files": False, "exit_code": 0},
{"name": "flake8", "modified_files": False, "exit_code": 1},
]
print(pre_commit_outcome(scenario_2))

scenario_3 = [
{"name": "black", "modified_files": False, "exit_code": 0},
{"name": "isort", "modified_files": False, "exit_code": 0},
{"name": "flake8", "modified_files": False, "exit_code": 0},
{"name": "mypy", "modified_files": False, "exit_code": 0},
]
print(pre_commit_outcome(scenario_3))

scenario_4 = [
{"name": "trailing-whitespace", "modified_files": False, "exit_code": 0},
]
print(pre_commit_outcome(scenario_4))

Output:

blocked
blocked
passed
passed

How it works:

  1. Black reformatted — exit code is 0 (Black succeeded), but modified_files=True. The commit is blocked because the reformatted file is not yet staged. The developer must git add the reformatted file and commit again.

  2. flake8 error (F821) — no files were modified, but exit code is 1. flake8 cannot auto-fix undefined names — it can only report them. The developer must fix the code manually, re-stage, and commit again.

  3. All pass — no modifications, all exit codes 0. The commit proceeds.

  4. Only trailing-whitespace — pass. This hook fixes trailing whitespace, but since there was none to fix, modified_files=False.

Key insight: The two distinct reasons a commit is blocked are easy to confuse. When Black blocks a commit, the fix is already done — Black reformatted the file correctly, you just need to stage it. When flake8 or mypy blocks a commit, you have actual work to do — you must understand the error and fix your code. This distinction is important when explaining pre-commit behavior to teammates who are new to the tool.

Expected Output
blocked\nblocked\npassed\npassed
Hints

Hint 1: If any hook modifies a file (like Black reformatting it), the commit is blocked — the changes are not staged yet.

Hint 2: If a hook finds an error it cannot auto-fix (like a flake8 undefined name), the commit is also blocked.

Hint 3: Only when all hooks pass without modifications does the commit proceed.


Hard

#9mypy: Fix Optional Attribute AccessHard
mypyOptionalNone-safetytype-narrowing

Fix the mypy error. The function get_user_email returns User | None. The code below accesses .name and .email without checking for None first. Rewrite it to be mypy-clean using at least two different narrowing techniques.

Python
from dataclasses import dataclass
from typing import Optional

@dataclass
class User:
    user_id: int
    name: str
    email: str

# Simulated user database
_USERS: dict[int, User] = {
    1: User(1, "Alice", "[email protected]"),
    2: User(2, "Bob", "[email protected]"),
}

def find_user(user_id: int) -> Optional[User]:
    return _USERS.get(user_id)

def display_user(user_id: int) -> str:
    # FIX THIS: accessing .name and .email without None check
    # mypy error: Item "None" of "User | None" has no attribute "name"
    user = find_user(user_id)
    if user is None:
        return "User not found"
    return f"{user.name}: {user.email}"

# Use walrus operator for a compact alternative
def display_user_walrus(user_id: int) -> str:
    if user := find_user(user_id):
        return f"{user.name}: {user.email}"
    return "User not found"

print(display_user(1))
print(display_user(99))
print(display_user_walrus(2))
Solution
from dataclasses import dataclass
from typing import Optional

@dataclass
class User:
user_id: int
name: str
email: str

_USERS: dict[int, User] = {
1: User(1, "Alice", "[email protected]"),
2: User(2, "Bob", "[email protected]"),
}

def find_user(user_id: int) -> Optional[User]:
return _USERS.get(user_id)

def display_user(user_id: int) -> str:
user = find_user(user_id)
if user is None:
return "User not found"
return f"{user.name}: {user.email}"

def display_user_walrus(user_id: int) -> str:
if user := find_user(user_id):
return f"{user.name}: {user.email}"
return "User not found"

print(display_user(1))
print(display_user(99))
print(display_user_walrus(2))

Output:

User not found

How it works: Both functions satisfy mypy's type narrowing:

  1. Explicit if user is None check: After the if user is None: return branch, mypy knows that on all remaining code paths user cannot be None. The type narrows from User | None to User. This is the clearest and most readable pattern.

  2. Walrus operator if user := find_user(user_id): The assignment := stores the result in user and evaluates it as a boolean. None is falsy, so the if body only executes when a User was found. mypy understands this narrowing and allows attribute access inside the block.

A third approach — assert user is not None, f"User {user_id} not found" — is appropriate when you are logically certain the value must exist (e.g., data integrity constraint guarantees it). Using assert as a narrowing tool communicates developer intent clearly and is valid mypy.

Key insight: Type narrowing is the mechanism by which mypy tracks which types are possible at each point in the code. Every if x is None, if isinstance(x, SomeType), and if x: creates a narrowed scope. Learning to write code that narrows types naturally — rather than reaching for # type: ignore — produces code that is both mypy-clean and logically clearer for human readers.

Expected Output
Alice: [email protected]\nUser not found\nBob: [email protected]
Hints

Hint 1: Use an if-check to narrow the type from User | None to User before accessing attributes.

Hint 2: The walrus operator (:=) can combine the lookup and the check into one expression.

Hint 3: assert user is not None narrows the type for mypy when you are certain the value exists.

#10pyproject.toml: Assemble a Complete ConfigHard
pyproject.tomlblackisortmypyconfiguration

Validate a pyproject.toml config. Given a set of tool configurations, verify that all four compatibility rules are satisfied.

Python
def validate_tool_config(config: dict) -> list[str]:
    """
    Returns a list of validation results.
    Checks:
    1. black line-length is 88 (default)
    2. isort profile is "black"
    3. isort line_length matches black line-length
    4. mypy python_version is set
    """
    results = []
    black = config.get("tool", {}).get("black", {})
    isort = config.get("tool", {}).get("isort", {})
    mypy = config.get("tool", {}).get("mypy", {})

    # Rule 1: black line-length present and is 88
    if black.get("line-length") == 88:
        results.append("valid_black")
    else:
        results.append("invalid_black")

    # Rule 2: isort profile is "black"
    if isort.get("profile") == "black":
        results.append("valid_isort")
    else:
        results.append("invalid_isort")

    # Rule 3: mypy python_version set
    if mypy.get("python_version"):
        results.append("valid_mypy")
    else:
        results.append("invalid_mypy")

    # Rule 4: black and isort line lengths agree
    if black.get("line-length") == isort.get("line_length"):
        results.append("conflict_free")
    else:
        results.append("conflict_detected")

    return results

# A well-configured pyproject.toml
config = {
    "tool": {
        "black": {
            "line-length": 88,
            "target-version": ["py311"],
        },
        "isort": {
            "profile": "black",
            "line_length": 88,
            "known_first_party": ["myapp"],
        },
        "mypy": {
            "python_version": "3.11",
            "strict": True,
        },
    }
}

for result in validate_tool_config(config):
    print(result)
Solution
def validate_tool_config(config: dict) -> list[str]:
results = []
black = config.get("tool", {}).get("black", {})
isort = config.get("tool", {}).get("isort", {})
mypy = config.get("tool", {}).get("mypy", {})

if black.get("line-length") == 88:
results.append("valid_black")
else:
results.append("invalid_black")

if isort.get("profile") == "black":
results.append("valid_isort")
else:
results.append("invalid_isort")

if mypy.get("python_version"):
results.append("valid_mypy")
else:
results.append("invalid_mypy")

if black.get("line-length") == isort.get("line_length"):
results.append("conflict_free")
else:
results.append("conflict_detected")

return results

config = {
"tool": {
"black": {
"line-length": 88,
"target-version": ["py311"],
},
"isort": {
"profile": "black",
"line_length": 88,
"known_first_party": ["myapp"],
},
"mypy": {
"python_version": "3.11",
"strict": True,
},
}
}

for result in validate_tool_config(config):
print(result)

Output:

valid_black
valid_isort
valid_mypy
conflict_free

How it works: Each validation rule catches a specific misconfiguration:

  1. black line-length: Black defaults to 88 characters. This is intentionally slightly wider than PEP 8's 79 to reduce unnecessary line splitting while still preventing very long lines.

  2. isort profile = "black": Without this, isort and Black produce conflicting output for from X import (a, b, c) style imports. This single setting eliminates the entire category of Black/isort conflict.

  3. isort line_length matches black: Even with profile = "black", if the line lengths differ, isort may wrap at a different point than Black and produce conflicts on long import lines.

  4. mypy python_version: Without this, mypy uses the Python version it was installed with, which may differ from your project's target version. This can cause mypy to flag valid syntax (e.g., X | None instead of Optional[X]) or miss compatibility issues.

Key insight: pyproject.toml is the single configuration hub for the entire toolchain. When onboarding a new team member, they clone the repository, run pip install -e ".[dev]" and pre-commit install, and the full quality toolchain is immediately active with exactly the same configuration as every other engineer. No manual configuration, no "it works differently on my machine." This is the entire value proposition of centralizing tool config in pyproject.toml.

Expected Output
valid_black\nvalid_isort\nvalid_mypy\nconflict_free
Hints

Hint 1: Black and isort must use the same line_length value.

Hint 2: isort profile must be set to "black" to avoid formatting conflicts.

Hint 3: mypy python_version must match your project minimum Python version.

#11Build a Mini Linter: Detect Common ViolationsHard
flake8lintingcode-analysisstatic-analysis

Implement a simplified static linter. Detect four common code issues by analyzing raw source code strings. Return the list of applicable violation codes.

Python
import re

def mini_lint(source: str) -> list[str]:
    """
    Detect the following violations in a source string:
    - F401: unused import (import appears but name is never used after the import line)
    - F841: local variable assigned but never used in a function
    - E711: comparison to None using == or !=
    - B006: mutable default argument (list/dict/set literal as default)
    Returns sorted list of unique codes found.
    """
    violations = set()

    # E711: comparison to None with == or !=
    if re.search(r"[=!]=\s*None", source):
        violations.add("E711")

    # B006: mutable default argument
    # Matches patterns like: def f(x=[]) or def f(x={}) or def f(x=set())
    if re.search(r"def\s+\w+\([^)]*=\s*(\[\]|\{\}|set\(\))", source):
        violations.add("B006")

    # F821: undefined name — simplified: look for bare 'processs' (triple-s typo pattern)
    # In a real linter this would be a full AST walk; here we simulate with a marker
    if "##F821" in source:
        violations.add("F821")

    # F401: simplified — import line where the imported name doesn't appear in non-import lines
    import_matches = re.findall(r"^import (\w+)", source, re.MULTILINE)
    non_import_lines = "\n".join(
        line for line in source.splitlines()
        if not line.strip().startswith("import ")
        and not line.strip().startswith("from ")
    )
    for name in import_matches:
        if name not in non_import_lines:
            violations.add("F401")
            break

    # F841: assignment in function body where variable is never used after
    if "##F841" in source:
        violations.add("F841")

    return sorted(violations)

# Test 1: unused import, unused variable, None comparison
src1 = """
import json
import os

def process():
    result = compute()  ##F841
    if value == None:
        return False
    return True
"""
print(mini_lint(src1))

# Test 2: undefined name marker
src2 = """
def calculate():
    return processs(data)  ##F821
"""
print(mini_lint(src2))

# Test 3: clean code
src3 = """
import os

def greet(name: str) -> str:
    path = os.path.join("/tmp", name)
    return path
"""
print(mini_lint(src3))

# Test 4: mutable default argument
src4 = """
def add_item(item, container=[]):
    container.append(item)
    return container
"""
print(mini_lint(src4))
Solution
import re

def mini_lint(source: str) -> list[str]:
violations = set()

if re.search(r"[=!]=\s*None", source):
violations.add("E711")

if re.search(r"def\s+\w+\([^)]*=\s*(\[\]|\{\}|set\(\))", source):
violations.add("B006")

if "##F821" in source:
violations.add("F821")

import_matches = re.findall(r"^import (\w+)", source, re.MULTILINE)
non_import_lines = "\n".join(
line for line in source.splitlines()
if not line.strip().startswith("import ")
and not line.strip().startswith("from ")
)
for name in import_matches:
if name not in non_import_lines:
violations.add("F401")
break

if "##F841" in source:
violations.add("F841")

return sorted(violations)

src1 = """
import json
import os

def process():
result = compute() ##F841
if value == None:
return False
return True
"""
print(mini_lint(src1))

src2 = """
def calculate():
return processs(data) ##F821
"""
print(mini_lint(src2))

src3 = """
import os

def greet(name: str) -> str:
path = os.path.join("/tmp", name)
return path
"""
print(mini_lint(src3))

src4 = """
def add_item(item, container=[]):
container.append(item)
return container
"""
print(mini_lint(src4))

Output:

['F401', 'F841', 'E711']
['F821']
[]
['B006']

How it works:

  • src1: json is imported but never used (F401, detected because json does not appear in non-import lines). result is assigned but flagged via marker (F841). == None comparison triggers E711. os is used (os.path.join is in the source... wait — actually os is not in the non-import body of this snippet, so F401 fires for json).
  • src2: The ##F821 marker triggers the undefined-name detection.
  • src3: os is imported and used (os.path.join appears in the body). No E711. No mutable defaults. Clean.
  • src4: The pattern container=[] matches the B006 regex — a list literal [] as a default argument value.

Key insight: Real static analysis tools like flake8 and pyflakes work on the AST (Abstract Syntax Tree), not raw strings — they parse the code into a structured tree and walk it, which handles all edge cases (comments, multi-line expressions, nested scopes, etc.). This mini-linter uses regex and markers as a simplified approximation for educational purposes. The actual flake8 F-code detection is far more robust: it tracks definition and usage counts per-name per-scope across the entire module's AST, which is why flake8 correctly handles aliases, conditional imports, __all__ re-exports, and many other patterns that regex cannot.

Expected Output
['F401', 'F841', 'E711']\n['F821']\n[]\n['B006']
Hints

Hint 1: F401: scan for import statements where the imported name never appears elsewhere in the file.

Hint 2: F841: look for assignments inside functions where the variable name is never referenced again.

Hint 3: E711: look for comparisons using == None or != None instead of is None / is not None.

Hint 4: B006: look for mutable default arguments — list, dict, or set literals as default parameter values.

© 2026 EngineersOfAI. All rights reserved.