Skip to main content

Python Project Structure Practice Problems & Exercises

Practice: Project Structure

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

Easy

#1What Makes a Directory a PackageEasy
packages__init__.pyimportable

Predict the output. This code checks which directories qualify as Python packages.

Python
import os
import tempfile

with tempfile.TemporaryDirectory() as root:
    # Create two directories — only one gets __init__.py
    pkg_dir = os.path.join(root, "mypackage")
    plain_dir = os.path.join(root, "notapackage")
    os.makedirs(pkg_dir)
    os.makedirs(plain_dir)

    # Add __init__.py to mypackage only
    with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
        f.write('__version__ = "1.0.0"\n')

    # Check which directory qualifies as a package
    pkg_init = os.path.join(pkg_dir, "__init__.py")
    plain_init = os.path.join(plain_dir, "__init__.py")

    print(os.path.isdir(pkg_dir))
    print(os.path.isfile(pkg_init))
    print(os.path.isfile(plain_init))
Solution
import os
import tempfile

with tempfile.TemporaryDirectory() as root:
pkg_dir = os.path.join(root, "mypackage")
plain_dir = os.path.join(root, "notapackage")
os.makedirs(pkg_dir)
os.makedirs(plain_dir)

with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
f.write('__version__ = "1.0.0"\n')

pkg_init = os.path.join(pkg_dir, "__init__.py")
plain_init = os.path.join(plain_dir, "__init__.py")

print(os.path.isdir(pkg_dir))
print(os.path.isfile(pkg_init))
print(os.path.isfile(plain_init))

Output:

True
True
False

How it works: mypackage/ is a directory (True) that contains __init__.py (True), making it a valid Python package. notapackage/ is also a directory, but has no __init__.py (False), so Python cannot import it as a package. Both directories exist on disk — the only difference is the presence or absence of __init__.py.

Key insight: The __init__.py file is the single signal that tells Python "this directory is a package, not just a folder." Without it, import mypackage raises ModuleNotFoundError. In Python 3.3+, namespace packages allow import from directories without __init__.py, but for regular packages (the vast majority of real-world use), you should always include it. At minimum, create an empty __init__.py; in production, use it to define your public API.

Expected Output
True\nTrue\nFalse
Hints

Hint 1: A directory becomes a Python package when it contains an __init__.py file.

Hint 2: os.path.isfile() checks whether a path exists and is a regular file.

Hint 3: Without __init__.py, a directory is just a directory — not importable as a package.

#2__init__.py: Version and __all__Easy
__init__.py__version____all__public-api

Predict the output. Inspect a package's __init__.py content to understand version metadata and public API declaration.

Python
import types

# Simulate what a well-written __init__.py would contain
init_code = """
__version__ = "1.2.0"
__author__ = "Engineering Team"

class User:
    pass

class AppError(Exception):
    pass

def process(data):
    return data

__all__ = ["User", "process", "AppError"]
"""

# Execute the init code in a fake module namespace
fake_module = types.ModuleType("myapp")
exec(init_code, fake_module.__dict__)

print(fake_module.__version__)
print(fake_module.__all__)
print("__author__" in fake_module.__dict__)
Solution
import types

init_code = """
__version__ = "1.2.0"
__author__ = "Engineering Team"

class User:
pass

class AppError(Exception):
pass

def process(data):
return data

__all__ = ["User", "process", "AppError"]
"""

fake_module = types.ModuleType("myapp")
exec(init_code, fake_module.__dict__)

print(fake_module.__version__)
print(fake_module.__all__)
print("__author__" in fake_module.__dict__)

Output:

1.2.0
['User', 'process', 'AppError']
True

How it works: __version__ is a plain string attribute — no magic, just convention. __all__ is a list of strings listing which names are exported when someone does from myapp import *. It also serves as documentation: a new developer reading __init__.py sees __all__ and immediately knows what the public API is.

Key insight: __all__ does NOT restrict direct imports — from myapp.services import internal_helper still works even if internal_helper is not in __all__. What __all__ controls is only the star import (import *) behavior and, by convention, signals which names are considered stable public API vs. internal implementation details. Linters like ruff can warn when someone imports a name not in __all__, enforcing the API boundary at development time.

Expected Output
1.2.0\n['User', 'process', 'AppError']\nTrue
Hints

Hint 1: __version__ is a module-level string attribute — by convention, assigned in __init__.py.

Hint 2: __all__ is a list of strings naming the public symbols that `from package import *` exports.

Hint 3: You can read module attributes with getattr(module, "__all__") or module.__all__.

#3Module Dependency DirectionEasy
module-boundariesdependency-directionarchitecture

Predict the output. A list of module names is sorted by their position in the correct dependency hierarchy — lowest-level first.

Python
# Standard dependency order for a well-structured application:
#   exceptions → nothing
#   utils      → nothing
#   models     → exceptions
#   services   → models, exceptions, config
#   routes     → services, models

# Represent each module's "level" in the dependency graph
module_levels = {
    "routes": 4,
    "services": 3,
    "models": 2,
    "utils": 1,
    "exceptions": 0,
}

# Sort from lowest-level (no deps) to highest-level (depends on everything)
ordered = sorted(module_levels.keys(), key=lambda m: module_levels[m])

for module in ordered:
    print(module)
Solution
module_levels = {
"routes": 4,
"services": 3,
"models": 2,
"utils": 1,
"exceptions": 0,
}

ordered = sorted(module_levels.keys(), key=lambda m: module_levels[m])

for module in ordered:
print(module)

Output:

exceptions
utils
models
services
routes

How it works: Each module has a "level" representing how far up the dependency stack it sits. exceptions and utils sit at level 0 and 1 — they import nothing from the application. models depends on exceptions. services depends on models and exceptions. routes sits at the top and can import from everything below it.

Key insight: One-directional dependency flow is the fundamental rule that prevents circular imports. If you think of the modules as a directed acyclic graph (DAG), edges should only point upward. Any time you find an arrow pointing downward — models importing from services, for instance — you have a circular dependency waiting to happen. The fix is always structural: extract the shared concept into a lower-level module that both can import without creating a cycle.

Expected Output
models\nexceptions\nutils\nservices\nroutes
Hints

Hint 1: The standard dependency order is: utils/exceptions (no deps) → models → services → routes.

Hint 2: Modules at the base of the hierarchy import nothing from the application.

Hint 3: Modules at the top (routes) can import from everything below them.

#4Script vs Package: The 200-Line HeuristicEasy
project-sizingscriptpackagedecision

Predict the output. A classifier determines whether a given scenario calls for a script or a package.

Python
def classify_project(lines, has_tests, imported_elsewhere, has_cli):
    """Determine whether a project should be a script or a package."""
    if imported_elsewhere:
        return "package"
    if has_tests:
        return "package"
    if lines > 200:
        return "package"
    return "script"

scenarios = [
    # (lines, has_tests, imported_elsewhere, has_cli)
    (80,   False, False, True),   # small automation script with CLI
    (350,  True,  False, True),   # medium tool with tests
    (120,  False, True,  False),  # shared utility imported by other scripts
    (50,   False, False, False),  # tiny one-off data processing task
]

for lines, tests, imported, cli in scenarios:
    print(classify_project(lines, tests, imported, cli))
Solution
def classify_project(lines, has_tests, imported_elsewhere, has_cli):
if imported_elsewhere:
return "package"
if has_tests:
return "package"
if lines > 200:
return "package"
return "script"

scenarios = [
(80, False, False, True),
(350, True, False, True),
(120, False, True, False),
(50, False, False, False),
]

for lines, tests, imported, cli in scenarios:
print(classify_project(lines, tests, imported, cli))

Output:

script
package
package
script

How it works:

  1. 80-line automation script with a CLI — small, standalone, not imported anywhere: script.
  2. 350-line tool with tests — exceeds 200 lines AND has tests: package.
  3. 120-line utility imported elsewhere — imported_elsewhere=True triggers package classification regardless of size.
  4. 50-line one-off task — small, no tests, not imported: script.

Key insight: The three triggers for promoting a script to a package are: it grows beyond ~200 lines, you want to write tests for it, or another script needs to import from it. Having a CLI (has_cli) alone is not sufficient — a small 80-line CLI can stay as a single script. Notice the priority order: imported_elsewhere is checked first because sharing logic always requires a package structure, no matter how small.

Expected Output
script\npackage\npackage\nscript
Hints

Hint 1: A script is a single file, never imported, under ~200 lines.

Hint 2: A package is needed when the file exceeds ~200 lines, needs tests, or is imported elsewhere.

Hint 3: Over-engineering a script into a package adds overhead without benefit.


Medium

#5Detecting Circular Import RiskMedium
circular-importsdependency-graphdesign

Detect circular imports by checking a dependency graph for cycles.

Python
def find_cycles(deps):
    """
    Find cycles in a dependency graph.
    deps: dict mapping module_name -> list of modules it imports from
    Returns a list of cycle descriptions (empty if no cycles).
    """
    cycles = []

    def dfs(start, current, path, visited):
        for neighbor in deps.get(current, []):
            if neighbor == start and len(path) > 1:
                cycles.append(" -> ".join(path + [neighbor]))
                return
            if neighbor not in visited:
                visited.add(neighbor)
                dfs(start, neighbor, path + [neighbor], visited)
                visited.discard(neighbor)

    for module in deps:
        dfs(module, module, [module], {module})

    return cycles

# BAD: circular dependency between models and services
bad_deps = {
    "models":   ["services"],   # models imports from services — wrong!
    "services": ["models"],     # services imports from models — expected
    "routes":   ["services"],
}

# GOOD: clean one-directional flow
good_deps = {
    "exceptions": [],
    "models":     ["exceptions"],
    "services":   ["models", "exceptions"],
    "routes":     ["services", "models"],
}

bad_cycles = find_cycles(bad_deps)
good_cycles = find_cycles(good_deps)

print(bad_cycles)
print("clean" if not good_cycles else good_cycles)
Solution
def find_cycles(deps):
cycles = []

def dfs(start, current, path, visited):
for neighbor in deps.get(current, []):
if neighbor == start and len(path) > 1:
cycles.append(" -> ".join(path + [neighbor]))
return
if neighbor not in visited:
visited.add(neighbor)
dfs(start, neighbor, path + [neighbor], visited)
visited.discard(neighbor)

for module in deps:
dfs(module, module, [module], {module})

return cycles

bad_deps = {
"models": ["services"],
"services": ["models"],
"routes": ["services"],
}

good_deps = {
"exceptions": [],
"models": ["exceptions"],
"services": ["models", "exceptions"],
"routes": ["services", "models"],
}

bad_cycles = find_cycles(bad_deps)
good_cycles = find_cycles(good_deps)

print(bad_cycles)
print("clean" if not good_cycles else good_cycles)

Output:

['models -> services -> models']
clean

How it works: The DFS traces paths from each starting module. When it finds a neighbor that equals the starting module (and the path has more than one hop), it records the cycle. In bad_deps, starting from models, we follow models -> services -> models and detect the cycle. In good_deps, following any path never returns to the start.

Key insight: Circular imports in Python cause ImportError: cannot import name 'X' from partially initialized module 'Y'. The root cause is that Python begins executing a module before it finishes importing it. If A starts importing and mid-way needs B, and B needs A, Python returns the partially-executed A — missing any names defined after the import statement. The solution is always structural: extract shared types into a models.py or types.py that both modules can import without needing each other.

Expected Output
['models -> services -> models']\nclean
Hints

Hint 1: A circular import exists when following the dependency chain eventually leads back to the starting module.

Hint 2: Model the dependencies as a dictionary and trace paths depth-first.

Hint 3: If any path visits the same module twice, a cycle exists.

#6__init__.py Public API Re-exportMedium
__init__.pyre-exportpublic-apirefactoring

Demonstrate re-export pattern. Show how __init__.py re-exports create a stable public API that decouples callers from internal module layout.

Python
import types
import sys

# Simulate myapp/models.py
models_code = """
class User:
    def __init__(self, name):
        self.name = name

class AppError(Exception):
    pass
"""

# Simulate myapp/services.py
services_code = """
def process(data):
    return data
"""

# Simulate myapp/__init__.py that re-exports the public API
init_code = """
from myapp.models import User, AppError
from myapp.services import process

__version__ = "1.0.0"
__all__ = ["User", "process", "AppError"]
"""

# Build the fake package
myapp = types.ModuleType("myapp")
myapp_models = types.ModuleType("myapp.models")
myapp_services = types.ModuleType("myapp.services")

exec(models_code, myapp_models.__dict__)
exec(services_code, myapp_services.__dict__)

sys.modules["myapp"] = myapp
sys.modules["myapp.models"] = myapp_models
sys.modules["myapp.services"] = myapp_services

exec(init_code, myapp.__dict__)

# Callers use the top-level package, not internal modules
print(hasattr(myapp, "User"))
print(hasattr(myapp, "process"))

# __all__ lists the stable public API
for name in myapp.__all__:
    print(name)
Solution
import types
import sys

models_code = """
class User:
def __init__(self, name):
self.name = name

class AppError(Exception):
pass
"""

services_code = """
def process(data):
return data
"""

init_code = """
from myapp.models import User, AppError
from myapp.services import process

__version__ = "1.0.0"
__all__ = ["User", "process", "AppError"]
"""

myapp = types.ModuleType("myapp")
myapp_models = types.ModuleType("myapp.models")
myapp_services = types.ModuleType("myapp.services")

exec(models_code, myapp_models.__dict__)
exec(services_code, myapp_services.__dict__)

sys.modules["myapp"] = myapp
sys.modules["myapp.models"] = myapp_models
sys.modules["myapp.services"] = myapp_services

exec(init_code, myapp.__dict__)

print(hasattr(myapp, "User"))
print(hasattr(myapp, "process"))

for name in myapp.__all__:
print(name)

Output:

True
True
User
process
AppError

How it works: By re-exporting User, process, and AppError in __init__.py, callers can write from myapp import User instead of from myapp.models import User. If you later refactor and move User into myapp/user.py, you just update the import in __init__.py — all callers stay unchanged.

Key insight: Re-exports in __init__.py create an abstraction boundary. The public API (what you export) is stable. The internal structure (which module each symbol lives in) is an implementation detail you can refactor freely. This is why well-maintained packages like requests, pydantic, and click let you import everything from the top-level package even though the internals are spread across many files.

Expected Output
True\nTrue\nUser\nprocess\nAppError
Hints

Hint 1: Re-exporting in __init__.py lets callers use `from myapp import X` instead of `from myapp.models import X`.

Hint 2: The internal module structure becomes an implementation detail hidden behind the public API.

Hint 3: __all__ should list every name that is part of the stable public interface.

#7src Layout: Detecting Import SourceMedium
src-layoutsys.pathpackaginginstallation

Demonstrate the src layout guarantee. Show that without installation, the src/ directory is not on sys.path.

Python
import sys
import os
import tempfile

with tempfile.TemporaryDirectory() as project_root:
    # Create src layout structure
    src_dir = os.path.join(project_root, "src")
    pkg_dir = os.path.join(src_dir, "mypackage")
    os.makedirs(pkg_dir)

    with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
        f.write('__version__ = "0.1.0"\n')

    # src/ is NOT automatically on sys.path
    src_on_path = src_dir in sys.path
    print(src_on_path)

    # Verify: try to find the package on current sys.path
    found_via_path = any(
        os.path.isdir(os.path.join(p, "mypackage"))
        for p in sys.path
        if p  # skip empty string entries
    )

    if not found_via_path:
        print("src not in sys.path")
    else:
        print("package found on sys.path")

    # With flat layout (package at root), it WOULD be found
    print("Installed package would be found via site-packages")
Solution
import sys
import os
import tempfile

with tempfile.TemporaryDirectory() as project_root:
src_dir = os.path.join(project_root, "src")
pkg_dir = os.path.join(src_dir, "mypackage")
os.makedirs(pkg_dir)

with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
f.write('__version__ = "0.1.0"\n')

src_on_path = src_dir in sys.path
print(src_on_path)

found_via_path = any(
os.path.isdir(os.path.join(p, "mypackage"))
for p in sys.path
if p
)

if not found_via_path:
print("src not in sys.path")
else:
print("package found on sys.path")

print("Installed package would be found via site-packages")

Output:

False
src not in sys.path
Installed package would be found via site-packages

How it works: The src/ directory is not on sys.path unless someone explicitly adds it or runs pip install -e .. This means import mypackage fails until the package is properly installed — which is exactly the point. With a flat layout (package at the project root), running pytest from the project root automatically finds the package because the root IS on sys.path.

Key insight: The src layout's main benefit is forcing a clean separation between the development tree and the installed package. A bug in pyproject.toml that accidentally excludes a file from the built package will cause tests to fail (because the installed package is broken) rather than pass against the raw source tree. This catches an entire class of packaging bugs before users discover them. The setup cost is one extra command: pip install -e ".[dev]" after cloning.

Expected Output
False\nsrc not in sys.path\nInstalled package would be found via site-packages
Hints

Hint 1: With src layout, the src/ directory is NOT on sys.path by default.

Hint 2: The package is only importable after pip install -e . adds it to site-packages.

Hint 3: This guarantees tests run against the installed package, not the raw source tree.

#8Splitting a Module into a Sub-packageMedium
sub-packagesrefactoringmodule-splitting__init__.py

Simulate splitting services.py into a sub-package. Verify that callers see no change in the public API.

Python
import types
import sys

# Before: everything in services.py
# After: services/ sub-package with auth.py and orders.py

# Simulate services/auth.py
auth_code = """
def authenticate(user_id):
    return user_id > 0

def create_session(user_id):
    return {"user_id": user_id, "token": "abc123"}
"""

# Simulate services/orders.py
orders_code = """
def create_order(user_id, items):
    return {"user_id": user_id, "items": items}

def cancel_order(order_id):
    return order_id
"""

# Simulate services/__init__.py — re-exports the full original API
services_init = """
from myapp.services.auth import authenticate, create_session
from myapp.services.orders import create_order, cancel_order

__all__ = ["authenticate", "create_session", "create_order", "cancel_order"]
"""

# Build the fake package tree
myapp = types.ModuleType("myapp")
services = types.ModuleType("myapp.services")
services_auth = types.ModuleType("myapp.services.auth")
services_orders = types.ModuleType("myapp.services.orders")

exec(auth_code, services_auth.__dict__)
exec(orders_code, services_orders.__dict__)

sys.modules["myapp"] = myapp
sys.modules["myapp.services"] = services
sys.modules["myapp.services.auth"] = services_auth
sys.modules["myapp.services.orders"] = services_orders

exec(services_init, services.__dict__)

# Callers use the same API as before the split
print(callable(services.authenticate))
print(callable(services.create_order))
print(callable(services.cancel_order))

# __all__ reflects the full public interface
for name in sorted(services.__all__):
    if not name.startswith("create_session"):
        print(name)
Solution
import types
import sys

auth_code = """
def authenticate(user_id):
return user_id > 0

def create_session(user_id):
return {"user_id": user_id, "token": "abc123"}
"""

orders_code = """
def create_order(user_id, items):
return {"user_id": user_id, "items": items}

def cancel_order(order_id):
return order_id
"""

services_init = """
from myapp.services.auth import authenticate, create_session
from myapp.services.orders import create_order, cancel_order

__all__ = ["authenticate", "create_session", "create_order", "cancel_order"]
"""

myapp = types.ModuleType("myapp")
services = types.ModuleType("myapp.services")
services_auth = types.ModuleType("myapp.services.auth")
services_orders = types.ModuleType("myapp.services.orders")

exec(auth_code, services_auth.__dict__)
exec(orders_code, services_orders.__dict__)

sys.modules["myapp"] = myapp
sys.modules["myapp.services"] = services
sys.modules["myapp.services.auth"] = services_auth
sys.modules["myapp.services.orders"] = services_orders

exec(services_init, services.__dict__)

print(callable(services.authenticate))
print(callable(services.create_order))
print(callable(services.cancel_order))

for name in sorted(services.__all__):
if not name.startswith("create_session"):
print(name)

Output:

True
True
True
authenticate
cancel_order
create_order

How it works: After splitting, services/__init__.py re-exports every public function from the sub-modules. Code that previously called from myapp.services import create_order still works exactly the same. The internal split into auth.py and orders.py is invisible to callers.

Key insight: The critical constraint when splitting a module into a sub-package is that the public API must remain identical. This is called an internal refactoring — the interface is closed for modification (callers see no change), but the implementation is open for extension (new sub-modules can be added freely). The practical trigger for a split is roughly 500 lines or more than three distinct responsibilities in a single file. Section-separator comments like # --- AUTH SECTION --- inside a file are a strong signal that a split is overdue.

Expected Output
True\nTrue\nTrue\ncreate_order\ncancel_order\nauthenticate
Hints

Hint 1: Converting a module to a sub-package means creating a directory with the same name.

Hint 2: The __init__.py of the sub-package re-exports the original public API.

Hint 3: Callers must NOT need to change their imports after the split.


Hard

#9__main__.py: Making a Package RunnableHard
__main__.pypython -mentry-pointscli

Build and verify a __main__.py entry point. Confirm the file exists, contains the expected entry point call, and executes correctly.

Python
import os
import sys
import tempfile
import subprocess

with tempfile.TemporaryDirectory() as root:
    # Create src layout package
    pkg_dir = os.path.join(root, "src", "myapp")
    os.makedirs(pkg_dir)

    # Write __init__.py
    with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
        f.write('__version__ = "1.0.0"\n')

    # Write routes.py with the actual CLI function
    with open(os.path.join(pkg_dir, "routes.py"), "w") as f:
        f.write(
            "import sys\n"
            "def cli():\n"
            "    print('myapp is running')\n"
            "    sys.exit(0)\n"
        )

    # Write __main__.py — the entry point for python -m myapp
    main_content = (
        '"""Entry point for python -m myapp."""\n'
        "from myapp.routes import cli\n"
        "\n"
        "if __name__ == '__main__':\n"
        "    cli()\n"
    )
    with open(os.path.join(pkg_dir, "__main__.py"), "w") as f:
        f.write(main_content)

    # Verify the file was created
    main_path = os.path.join(pkg_dir, "__main__.py")
    print(os.path.isfile(main_path))
    print("__main__.py found" if os.path.isfile(main_path) else "missing")

    # Run the package with python -m myapp
    result = subprocess.run(
        [sys.executable, "-m", "myapp"],
        cwd=os.path.join(root, "src"),
        capture_output=True,
        text=True,
    )
    print(result.stdout.strip())
    print(result.returncode)
Solution
import os
import sys
import tempfile
import subprocess

with tempfile.TemporaryDirectory() as root:
pkg_dir = os.path.join(root, "src", "myapp")
os.makedirs(pkg_dir)

with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
f.write('__version__ = "1.0.0"\n')

with open(os.path.join(pkg_dir, "routes.py"), "w") as f:
f.write(
"import sys\n"
"def cli():\n"
" print('myapp is running')\n"
" sys.exit(0)\n"
)

main_content = (
'"""Entry point for python -m myapp."""\n'
"from myapp.routes import cli\n"
"\n"
"if __name__ == '__main__':\n"
" cli()\n"
)
with open(os.path.join(pkg_dir, "__main__.py"), "w") as f:
f.write(main_content)

main_path = os.path.join(pkg_dir, "__main__.py")
print(os.path.isfile(main_path))
print("__main__.py found" if os.path.isfile(main_path) else "missing")

result = subprocess.run(
[sys.executable, "-m", "myapp"],
cwd=os.path.join(root, "src"),
capture_output=True,
text=True,
)
print(result.stdout.strip())
print(result.returncode)

Output:

True
__main__.py found
myapp is running
0

How it works: When Python receives python -m myapp, it looks for a __main__.py inside the myapp package on sys.path. We run the subprocess with cwd=src/ so that Python can find the myapp directory directly (simulating what pip install -e . would do via site-packages). The __main__.py imports cli from routes.py and calls it, which prints the message and exits with code 0.

Key insight: __main__.py should always be minimal — typically two lines: the import and the call. The real logic lives in routes.py (or wherever your CLI is defined) so it can be tested independently. This design means the same cli() function powers both python -m myapp and the installed myapp script defined in pyproject.toml [project.scripts]. Two invocations, one code path. __main__.py is especially valuable in Docker containers and CI environments where running python -m myapp is more reliable than depending on the installed script entry point being on PATH.

Expected Output
True\n__main__.py found\nmyapp is running\n0
Hints

Hint 1: __main__.py is executed when you run python -m mypackage from the command line.

Hint 2: It should be minimal — import and call the main function, nothing else.

Hint 3: The installed script entry point (pyproject.toml [project.scripts]) and __main__.py should call the same function.

#10pyproject.toml: Parsing and Validating ConfigurationHard
pyproject.tomlpackagingconfigurationbuild-system

Parse and validate a pyproject.toml configuration. Extract key fields and verify the structure is correct.

Python
import tomllib
import io

# A minimal but complete pyproject.toml
toml_content = b"""
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "myapp"
version = "0.1.0"
description = "A professional Python application"
requires-python = ">=3.11"
dependencies = [
    "click>=8.1",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "mypy>=1.8",
    "ruff>=0.3",
]

[project.scripts]
myapp = "myapp.routes:cli"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--tb=short"
"""

config = tomllib.loads(toml_content.decode())

project = config["project"]
print(project["name"])
print(project["version"])
print(project["requires-python"])
print(project["dependencies"])

# Verify optional dependencies include dev tools
dev_deps = project.get("optional-dependencies", {}).get("dev", [])
has_pytest = any("pytest" in d for d in dev_deps)
has_mypy = any("mypy" in d for d in dev_deps)
print(has_pytest)
print(has_mypy)
Solution
import tomllib

toml_content = b"""
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "myapp"
version = "0.1.0"
description = "A professional Python application"
requires-python = ">=3.11"
dependencies = [
"click>=8.1",
"pydantic>=2.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0",
"mypy>=1.8",
"ruff>=0.3",
]

[project.scripts]
myapp = "myapp.routes:cli"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--tb=short"
"""

config = tomllib.loads(toml_content.decode())

project = config["project"]
print(project["name"])
print(project["version"])
print(project["requires-python"])
print(project["dependencies"])

dev_deps = project.get("optional-dependencies", {}).get("dev", [])
has_pytest = any("pytest" in d for d in dev_deps)
has_mypy = any("mypy" in d for d in dev_deps)
print(has_pytest)
print(has_mypy)

Output:

myapp
0.1.0
>=3.11
['click>=8.1', 'pydantic>=2.0']
True
True

How it works: tomllib (Python 3.11+ stdlib) parses TOML into a nested dictionary. The [project] table maps directly to config["project"]. Dependencies under [project.optional-dependencies] are accessed via the nested key path. The any() call checks whether any dependency string contains the target package name.

Key insight: pyproject.toml replaced five separate config files (setup.py, setup.cfg, MANIFEST.in, tox.ini, and .flake8). The key structural rules: dependencies lists what every user needs; optional-dependencies groups tools that only specific users need (never include pytest in dependencies). Pin minimum versions (>=8.0) in library dependencies — exact pinning (==8.0.0) forces conflicts on users. For applications (not libraries), use a lockfile generated by pip-compile or uv for exact reproducibility, not pinned versions in pyproject.toml itself.

Expected Output
myapp\n0.1.0\n>=3.11\n['click>=8.1', 'pydantic>=2.0']\nTrue\nTrue
Hints

Hint 1: pyproject.toml uses TOML format — Python stdlib has a tomllib module (3.11+) for reading it.

Hint 2: The [project] table holds name, version, requires-python, and dependencies.

Hint 3: [project.optional-dependencies] holds extra groups like dev and docs.

#11Full Project Scaffold GeneratorHard
project-scaffoldsrc-layoutautomationpathlib

Build a project scaffold generator that creates a complete src-layout Python project from a given name. Verify all required files and directories are created.

Python
from pathlib import Path
import tempfile

def scaffold_project(name: str, root: Path) -> None:
    """Generate a complete src-layout Python project scaffold."""
    project = root / name

    # Create directory structure
    pkg_dir = project / "src" / name
    tests_dir = project / "tests"
    docs_dir = project / "docs"

    for d in [pkg_dir, tests_dir, docs_dir]:
        d.mkdir(parents=True, exist_ok=True)

    # Package modules
    modules = {
        "__init__.py": (
            f'"""{ name } package."""\n\n'
            '__version__ = "0.1.0"\n'
        ),
        "__main__.py": (
            f'"""Entry point for python -m {name}."""\n'
            f"from {name}.routes import cli\n\n"
            "if __name__ == '__main__':\n"
            "    cli()\n"
        ),
        "models.py":     "# Data structures\n",
        "services.py":   "# Business logic\n",
        "routes.py":     (
            "import sys\n\n"
            "def cli() -> None:\n"
            f'    print("{name} is running")\n'
            "    sys.exit(0)\n"
        ),
        "utils.py":      "# Generic helpers\n",
        "exceptions.py": (
            f"class {name.title()}Error(Exception):\n"
            "    pass\n"
        ),
    }

    for filename, content in modules.items():
        (pkg_dir / filename).write_text(content)

    # Top-level files
    (project / "pyproject.toml").write_text(
        "[build-system]\n"
        'requires = ["setuptools>=68"]\n'
        'build-backend = "setuptools.backends.legacy:build"\n\n'
        "[project]\n"
        f'name = "{name}"\n'
        'version = "0.1.0"\n'
        'requires-python = ">=3.11"\n'
        "dependencies = []\n\n"
        "[project.optional-dependencies]\n"
        'dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.3"]\n\n'
        "[tool.setuptools.packages.find]\n"
        'where = ["src"]\n'
    )
    (project / ".gitignore").write_text(
        "__pycache__/\n*.py[cod]\ndist/\nbuild/\n"
        "*.egg-info/\n.venv/\n.env\n.pytest_cache/\n"
    )
    (project / "README.md").write_text(f"# {name}\n\nA short description.\n")
    (tests_dir / "__init__.py").write_text("")
    (tests_dir / "conftest.py").write_text("# pytest fixtures\n")


with tempfile.TemporaryDirectory() as tmp:
    root = Path(tmp)
    scaffold_project("myapp", root)

    project = root / "myapp"
    pkg_dir = project / "src" / "myapp"

    # Verify critical structural elements
    print((project / "pyproject.toml").is_file())
    print((project / ".gitignore").is_file())
    print((project / "tests" / "conftest.py").is_file())
    print((pkg_dir / "__init__.py").is_file())
    print((pkg_dir / "__main__.py").is_file())

    # List all package modules
    pkg_files = sorted(f.name for f in pkg_dir.iterdir() if f.suffix == ".py")
    print(pkg_files)
Solution
from pathlib import Path
import tempfile

def scaffold_project(name: str, root: Path) -> None:
project = root / name

pkg_dir = project / "src" / name
tests_dir = project / "tests"
docs_dir = project / "docs"

for d in [pkg_dir, tests_dir, docs_dir]:
d.mkdir(parents=True, exist_ok=True)

modules = {
"__init__.py": (
f'"""{ name } package."""\n\n'
'__version__ = "0.1.0"\n'
),
"__main__.py": (
f'"""Entry point for python -m {name}."""\n'
f"from {name}.routes import cli\n\n"
"if __name__ == '__main__':\n"
" cli()\n"
),
"models.py": "# Data structures\n",
"services.py": "# Business logic\n",
"routes.py": (
"import sys\n\n"
"def cli() -> None:\n"
f' print("{name} is running")\n'
" sys.exit(0)\n"
),
"utils.py": "# Generic helpers\n",
"exceptions.py": (
f"class {name.title()}Error(Exception):\n"
" pass\n"
),
}

for filename, content in modules.items():
(pkg_dir / filename).write_text(content)

(project / "pyproject.toml").write_text(
"[build-system]\n"
'requires = ["setuptools>=68"]\n'
'build-backend = "setuptools.backends.legacy:build"\n\n'
"[project]\n"
f'name = "{name}"\n'
'version = "0.1.0"\n'
'requires-python = ">=3.11"\n'
"dependencies = []\n\n"
"[project.optional-dependencies]\n"
'dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.3"]\n\n'
"[tool.setuptools.packages.find]\n"
'where = ["src"]\n'
)
(project / ".gitignore").write_text(
"__pycache__/\n*.py[cod]\ndist/\nbuild/\n"
"*.egg-info/\n.venv/\n.env\n.pytest_cache/\n"
)
(project / "README.md").write_text(f"# {name}\n\nA short description.\n")
(tests_dir / "__init__.py").write_text("")
(tests_dir / "conftest.py").write_text("# pytest fixtures\n")


with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
scaffold_project("myapp", root)

project = root / "myapp"
pkg_dir = project / "src" / "myapp"

print((project / "pyproject.toml").is_file())
print((project / ".gitignore").is_file())
print((project / "tests" / "conftest.py").is_file())
print((pkg_dir / "__init__.py").is_file())
print((pkg_dir / "__main__.py").is_file())

pkg_files = sorted(f.name for f in pkg_dir.iterdir() if f.suffix == ".py")
print(pkg_files)

Output:

True
True
True
True
True
['__init__.py', '__main__.py', 'exceptions.py', 'models.py', 'routes.py', 'services.py', 'utils.py']

How it works: The scaffold generator uses pathlib.Path throughout — Path.mkdir(parents=True, exist_ok=True) creates the full directory chain, and Path.write_text() creates files in one call. The function creates all seven standard package modules, the top-level pyproject.toml, .gitignore, README.md, and the test directory with conftest.py. The final verification reads the package directory and sorts all .py filenames alphabetically.

Key insight: This is the same pattern used by tools like cookiecutter, hatch new, and uv init. The value is repeatability — every project starts from the same correct structure rather than accumulating different ad-hoc layouts. Notice the use of string concatenation for file contents rather than f-strings: this avoids the MDX/JSX f-string {} gotcha entirely while still producing valid Python. In a real scaffold tool, you would use Jinja2 templates stored in a templates/ directory for more complex file generation. After scaffolding, the workflow is always: python -m venv .venv, source .venv/bin/activate, pip install -e ".[dev]", then pytest to confirm everything works.

Expected Output
True\nTrue\nTrue\nTrue\nTrue\n['__init__.py', '__main__.py', 'exceptions.py', 'models.py', 'routes.py', 'services.py', 'utils.py']
Hints

Hint 1: Use pathlib.Path for all file and directory creation — it is more readable than os.makedirs + open.

Hint 2: Path.mkdir(parents=True, exist_ok=True) creates the full directory chain.

Hint 3: Path.write_text() creates a file and writes its content in one call.

© 2026 EngineersOfAI. All rights reserved.