Skip to main content

Python pyproject.toml Practice Problems & Exercises

Practice: pyproject.toml

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

Easy

#1Parse Project MetadataEasy
tomllibproject-metadataPEP-621

Parse a pyproject.toml string and extract the standard [project] metadata. This is what pip, build tools, and package registries read to understand your project.

Python
try:
    import tomllib
except ImportError:
    try:
        import tomli as tomllib
    except ImportError:
        tomllib = None

def parse_project_metadata(toml_content):
    if tomllib is None:
        # Minimal fallback: extract name with simple parsing
        name = None
        for line in toml_content.splitlines():
            if line.strip().startswith('name'):
                _, _, val = line.partition('=')
                name = val.strip().strip('"\'')
                break
        return {'name': name, 'version': None, 'description': None,
                'requires_python': None, 'dependencies': [], 'authors': []}

    data = tomllib.loads(toml_content)
    project = data.get('project', {})
    return {
        'name': project.get('name'),
        'version': project.get('version'),
        'description': project.get('description'),
        'requires_python': project.get('requires-python'),
        'dependencies': project.get('dependencies', []),
        'authors': project.get('authors', []),
    }

sample = """
[project]
name = "mypackage"
version = "1.0.0"
description = "A sample package"
requires-python = ">=3.9"
dependencies = ["requests>=2.28"]

[[project.authors]]
name = "AI Engineer"
email = "[email protected]"
"""
result = parse_project_metadata(sample)
print(result['name'], result['version'])
Solution
try:
import tomllib
except ImportError:
import tomli as tomllib

def parse_project_metadata(toml_content):
data = tomllib.loads(toml_content)
project = data.get('project', {})
return {
'name': project.get('name'),
'version': project.get('version'),
'description': project.get('description'),
'requires_python': project.get('requires-python'),
'dependencies': project.get('dependencies', []),
'authors': project.get('authors', []),
}

tomllib was added to the Python stdlib in 3.11. For older Pythons, install tomli (the backport). Notice requires-python uses a hyphen in TOML but most Python code uses requires_python with an underscore — the hyphen is the standard PEP 621 key name. The [[project.authors]] syntax creates an array of tables — each [[...]] block appends an entry to the list. This is TOML's way of expressing a list of objects.

def parse_project_metadata(toml_content):
    """Parse a pyproject.toml string and extract
    standard [project] metadata.
    
    Return a dict with keys (all from [project] table):
    - 'name': str
    - 'version': str or None
    - 'description': str or None
    - 'requires_python': str or None
    - 'dependencies': list of str
    - 'authors': list of dicts with 'name' and/or 'email'
    
    Use the tomllib module (Python 3.11+) or fallback parsing.
    """
    # TODO: implement
    pass
Expected Output
{'name': 'mypackage', 'version': '1.0.0', 'description': 'A sample package', 'requires_python': '>=3.9', 'dependencies': ['requests>=2.28'], 'authors': [{'name': 'AI Engineer', 'email': '[email protected]'}]}
Hints

Hint 1: Use tomllib.loads(toml_content) to parse the TOML string into a dict.

Hint 2: The [project] table maps to data["project"]. Access sub-keys with .get() for optional fields.

#2Validate Required Metadata FieldsEasy
validationPEP-621metadata

Write a validator for [project] table contents. Build tools like hatchling and flit run similar validation before accepting a pyproject.toml.

Python
import re

def validate_project_table(project_dict):
    errors = []

    if 'name' not in project_dict or not project_dict['name']:
        errors.append('name is required')
    elif not isinstance(project_dict['name'], str):
        errors.append('name must be a string')

    if 'version' not in project_dict or not project_dict['version']:
        errors.append('version is required')
    elif not isinstance(project_dict['version'], str):
        errors.append('version must be a string')

    if 'requires-python' in project_dict:
        rp = project_dict['requires-python']
        if not re.match(r'^[><=!~]', rp):
            errors.append('requires-python must start with a version operator')

    if 'dependencies' in project_dict:
        if not isinstance(project_dict['dependencies'], list):
            errors.append('dependencies must be a list')

    return {'valid': len(errors) == 0, 'errors': errors}

print(validate_project_table({}))
print(validate_project_table({'name': 'mypkg', 'version': '1.0.0'}))
print(validate_project_table({'name': 'mypkg', 'version': '1.0.0', 'requires-python': '>=3.9'}))
Solution
import re

def validate_project_table(project_dict):
errors = []
if 'name' not in project_dict or not project_dict['name']:
errors.append('name is required')
elif not isinstance(project_dict['name'], str):
errors.append('name must be a string')
if 'version' not in project_dict or not project_dict['version']:
errors.append('version is required')
elif not isinstance(project_dict['version'], str):
errors.append('version must be a string')
if 'requires-python' in project_dict:
rp = project_dict['requires-python']
if not re.match(r'^[><=!~]', rp):
errors.append('requires-python must start with a version operator')
if 'dependencies' in project_dict:
if not isinstance(project_dict['dependencies'], list):
errors.append('dependencies must be a list')
return {'valid': len(errors) == 0, 'errors': errors}

PEP 621 makes name and version the only mandatory fields, though build backends may require additional fields. The name field is normalized: my-package, my_package, and mypackage are all the same package (normalization replaces hyphens and underscores with - and lowercases). PyPI enforces this normalization — you cannot register my-package and my_package as different packages. The validate-pyproject CLI tool provides full PEP 621 validation using JSON Schema.

def validate_project_table(project_dict):
    """Validate that a project metadata dict has all
    required fields and that they have correct types.
    
    Required: name (str), version (str)
    Optional but validated if present:
    - requires-python: must match pattern like '>=3.9'
    - dependencies: must be a list
    
    Return a dict:
    - 'valid': bool
    - 'errors': list of error strings
    """
    # TODO: implement
    pass
Expected Output
{'valid': False, 'errors': ['name is required', 'version is required']}
Hints

Hint 1: Check for key existence with "key in project_dict". Check type with isinstance().

Hint 2: For requires-python, use a regex to validate the pattern starts with a valid operator.

#3Extract Entry PointsEasy
entry-pointsconsole-scriptsPEP-621

Parse console script entry points from [project.scripts]. When pip installs a package with scripts defined, it creates wrapper executables in venv/bin/ that call the specified Python functions.

Python
def extract_entry_points(project_dict):
    scripts = project_dict.get('scripts', {})
    result = []
    for name, target in scripts.items():
        if ':' in target:
            module, _, function = target.partition(':')
            result.append({
                'name': name,
                'module': module.strip(),
                'function': function.strip(),
            })
    return result

project = {
    'name': 'mypackage',
    'scripts': {
        'mycli': 'mypackage.cli:main',
        'myserver': 'mypackage.server:run',
    }
}
print(extract_entry_points(project))
Solution
def extract_entry_points(project_dict):
scripts = project_dict.get('scripts', {})
result = []
for name, target in scripts.items():
if ':' in target:
module, _, function = target.partition(':')
result.append({
'name': name,
'module': module.strip(),
'function': function.strip(),
})
return result

Entry points are how pip creates executable CLI commands. When pip installs a package with [project.scripts], it generates a small Python wrapper script in venv/bin/ that imports the module and calls the function. This is how tools like black, pytest, flask, and uvicorn become available as commands after pip install. The [project.entry-points] table is the more general form — it allows plugins to register themselves for any named group (e.g., pytest11 for pytest plugins, babel.extractors for Babel).

def extract_entry_points(project_dict):
    """Extract console script entry points from a project dict.
    
    project_dict may contain:
    [project.scripts]
    mycli = "mypackage.cli:main"
    
    Return a list of dicts:
    - 'name': the CLI command name
    - 'module': the Python module path
    - 'function': the function name
    
    Example: 'mycli = "mypackage.cli:main"' ->
        {'name': 'mycli', 'module': 'mypackage.cli', 'function': 'main'}
    """
    # TODO: implement
    pass
Expected Output
[{'name': 'mycli', 'module': 'mypackage.cli', 'function': 'main'}]
Hints

Hint 1: Scripts are in project_dict.get("scripts", {}). Each key is the command name, value is "module:function".

Hint 2: Split the value on ":" to get module and function.

#4Identify Build BackendEasy
build-systemPEP-518build-backend

Write a backend identifier for pyproject.toml files. Different backends have different feature sets and configuration tables — knowing which backend a project uses is the first step in any automated build system interaction.

Python
def identify_build_backend(toml_dict):
    build_system = toml_dict.get('build-system', {})
    backend_path = build_system.get('build-backend', '')
    requires = build_system.get('requires', [])

    mapping = {
        'hatchling': 'hatchling',
        'setuptools': 'setuptools',
        'flit_core': 'flit',
        'poetry': 'poetry',
        'maturin': 'maturin',
        'pdm': 'pdm-backend',
    }

    backend = 'unknown'
    for key, name in mapping.items():
        if key in backend_path:
            backend = name
            break

    return {
        'backend': backend,
        'backend_path': backend_path,
        'requires': requires,
    }

data = {
    'build-system': {
        'requires': ['hatchling'],
        'build-backend': 'hatchling.build',
    }
}
print(identify_build_backend(data))
Solution
def identify_build_backend(toml_dict):
build_system = toml_dict.get('build-system', {})
backend_path = build_system.get('build-backend', '')
requires = build_system.get('requires', [])
mapping = {
'hatchling': 'hatchling', 'setuptools': 'setuptools',
'flit_core': 'flit', 'poetry': 'poetry',
'maturin': 'maturin', 'pdm': 'pdm-backend',
}
backend = 'unknown'
for key, name in mapping.items():
if key in backend_path:
backend = name
break
return {'backend': backend, 'backend_path': backend_path, 'requires': requires}

The [build-system] table was standardised in PEP 518 (2016) to declare build dependencies separately from runtime dependencies. Before PEP 518, pip had to guess that a project needed setuptools and install it before anything else could happen. Now pip reads build-system.requires, installs exactly those packages into an isolated build environment, then calls build-system.build-backend to produce the wheel. This isolation means build tools do not pollute your runtime environment.

def identify_build_backend(toml_dict):
    """Identify the build backend from a parsed pyproject.toml.
    
    Check [build-system].build-backend.
    Return one of: 'hatchling', 'setuptools', 'flit', 'poetry',
    'maturin', 'pdm-backend', or 'unknown'.
    
    Also return the requires list from [build-system].
    
    Return a dict:
    - 'backend': str
    - 'backend_path': str (the full build-backend value)
    - 'requires': list of str
    """
    # TODO: implement
    pass
Expected Output
{'backend': 'hatchling', 'backend_path': 'hatchling.build', 'requires': ['hatchling']}
Hints

Hint 1: Access toml_dict.get("build-system", {}).get("build-backend", "").

Hint 2: Map the backend path to a short name: "hatchling.build" -> "hatchling", "setuptools.build_meta" -> "setuptools".


Medium

#5Optional Dependencies ResolverMedium
optional-dependenciesextrasPEP-621

Resolve the full dependency set for a package installed with extras — pip install mypackage[web,aws]. The result is the base dependencies plus all deps from the requested extra groups.

Python
import re

def extract_pkg_name(dep):
    return re.split(r'[>=<!~\[;\s]', dep.strip())[0].lower()

def resolve_extras(project_dict, requested_extras):
    all_deps = list(project_dict.get('dependencies', []))
    optional = project_dict.get('optional-dependencies', {})

    for extra in requested_extras:
        if extra in optional:
            all_deps.extend(optional[extra])

    # Deduplicate by package name, keeping last occurrence
    seen = {}
    for dep in all_deps:
        name = extract_pkg_name(dep)
        seen[name] = dep

    return sorted(seen.values(), key=extract_pkg_name)

project = {
    'dependencies': ['requests>=2.28'],
    'optional-dependencies': {
        'web': ['uvicorn>=0.23', 'fastapi>=0.100'],
        'aws': ['boto3>=1.28'],
        'dev': ['pytest>=7.0', 'mypy>=1.0'],
    }
}
print(resolve_extras(project, ['web', 'aws']))
Solution
import re

def extract_pkg_name(dep):
return re.split(r'[>=<!~\[;\s]', dep.strip())[0].lower()

def resolve_extras(project_dict, requested_extras):
all_deps = list(project_dict.get('dependencies', []))
optional = project_dict.get('optional-dependencies', {})
for extra in requested_extras:
if extra in optional:
all_deps.extend(optional[extra])
seen = {}
for dep in all_deps:
seen[extract_pkg_name(dep)] = dep
return sorted(seen.values(), key=extract_pkg_name)

Optional dependencies are pip's way of expressing conditional feature sets. pip install requests[security] installs the cryptography and pyOpenSSL packages that are not needed for basic HTTP but required for certificate verification. pip install fastapi[all] installs all optional server/testing extras. This lets package authors keep a minimal install fast while offering a batteries-included option. The [dev] extra convention is used with pip install -e ".[dev]" to install development tools without making them runtime requirements.

def resolve_extras(project_dict, requested_extras):
    """Resolve which dependencies to install given
    a set of requested extras.
    
    project_dict has:
    - 'dependencies': base deps list
    - 'optional-dependencies': dict of extra -> deps list
    
    requested_extras: list of extra names
    
    Return a sorted, deduplicated list of all deps
    (base + requested extras combined).
    """
    # TODO: implement
    pass
Expected Output
['boto3>=1.28', 'requests>=2.28', 'uvicorn>=0.23']
Hints

Hint 1: Start with the base dependencies list. For each requested extra, extend with its deps.

Hint 2: Normalise package names (lowercase) before deduplication to handle case differences.

#6Generate pyproject.toml from MetadataMedium
code-generationTOMLPEP-621

Generate a complete pyproject.toml string from structured metadata. This is what hatch new, poetry new, and project scaffolding tools do when you run them.

Python
def generate_pyproject_toml(metadata):
    deps = metadata.get('dependencies', [])
    dev_deps = metadata.get('dev_dependencies', [])

    def fmt_list(items):
        if not items:
            return '[]'
        lines = ['[']
        for item in items:
            lines.append('  "' + item + '",')
        lines.append(']')
        return '\n'.join(lines)

    parts = [
        '[build-system]',
        'requires = ["hatchling"]',
        'build-backend = "hatchling.build"',
        '',
        '[project]',
        'name = "' + metadata['name'] + '"',
        'version = "' + metadata['version'] + '"',
        'description = "' + metadata.get('description', '') + '"',
        'requires-python = "' + metadata.get('requires_python', '>=3.9') + '"',
        'dependencies = ' + fmt_list(deps),
        '',
        '[[project.authors]]',
        'name = "' + metadata.get('author_name', '') + '"',
        'email = "' + metadata.get('author_email', '') + '"',
    ]

    if dev_deps:
        parts += [
            '',
            '[project.optional-dependencies]',
            'dev = ' + fmt_list(dev_deps),
        ]

    return '\n'.join(parts)

meta = {
    'name': 'mypackage',
    'version': '0.1.0',
    'description': 'My awesome package',
    'requires_python': '>=3.10',
    'author_name': 'AI Engineer',
    'author_email': '[email protected]',
    'dependencies': ['requests>=2.28', 'click>=8.0'],
    'dev_dependencies': ['pytest>=7.0', 'mypy>=1.0'],
}
print(generate_pyproject_toml(meta))
Solution
def generate_pyproject_toml(metadata):
deps = metadata.get('dependencies', [])
dev_deps = metadata.get('dev_dependencies', [])
def fmt_list(items):
if not items:
return '[]'
return '[\n' + '\n'.join(' "' + i + '",' for i in items) + '\n]'
parts = [
'[build-system]', 'requires = ["hatchling"]', 'build-backend = "hatchling.build"', '',
'[project]', 'name = "' + metadata['name'] + '"', 'version = "' + metadata['version'] + '"',
'description = "' + metadata.get('description', '') + '"',
'requires-python = "' + metadata.get('requires_python', '>=3.9') + '"',
'dependencies = ' + fmt_list(deps), '',
'[[project.authors]]',
'name = "' + metadata.get('author_name', '') + '"',
'email = "' + metadata.get('author_email', '') + '"',
]
if dev_deps:
parts += ['', '[project.optional-dependencies]', 'dev = ' + fmt_list(dev_deps)]
return '\n'.join(parts)

TOML generation requires careful attention to quoting and nesting. Project scaffolding tools like cookiecutter, copier, and hatch new use template engines (Jinja2) rather than string concatenation to generate these files safely. The section order in pyproject.toml is conventionally: [build-system] first, then [project], then optional-dependencies, then tool configs like [tool.pytest], [tool.mypy], [tool.ruff]. Following this convention makes the file readable to humans and consistent across projects.

def generate_pyproject_toml(metadata):
    """Generate a pyproject.toml string from a metadata dict.
    
    metadata keys:
    - name: str
    - version: str
    - description: str
    - requires_python: str (e.g. '>=3.9')
    - author_name: str
    - author_email: str
    - dependencies: list of str
    - dev_dependencies: list of str (optional extras)
    
    Use hatchling as the build backend.
    Return a properly formatted TOML string.
    """
    # TODO: implement
    pass
Expected Output
[build-system]
requires = ["hatchling"]
...
Hints

Hint 1: Build the string section by section. For lists, format each item as " \"dep\"," on its own line.

Hint 2: The [build-system] section must come before [project] by convention.

#7Src Layout ValidatorMedium
src-layoutproject-structureimportability

Validate that a project follows the src layout convention. The src layout prevents accidental imports of the development version of a package — when running tests, Python finds the installed version in site-packages, not the local source tree.

Python
import os

def validate_src_layout(project_root):
    errors = []
    warnings = []
    package_name = None

    if not os.path.exists(os.path.join(project_root, 'pyproject.toml')):
        errors.append('pyproject.toml not found at project root')

    src_dir = os.path.join(project_root, 'src')
    if not os.path.isdir(src_dir):
        errors.append('src/ directory not found')
    else:
        packages = [
            d for d in os.listdir(src_dir)
            if os.path.isdir(os.path.join(src_dir, d))
            and not d.startswith('.')
            and not d.endswith('.egg-info')
        ]
        if not packages:
            errors.append('No Python package found inside src/')
        else:
            package_name = packages[0]
            init_path = os.path.join(src_dir, package_name, '__init__.py')
            if not os.path.exists(init_path):
                warnings.append('src/' + package_name + '/__init__.py not found')

    tests_dir = os.path.join(project_root, 'tests')
    if not os.path.isdir(tests_dir):
        warnings.append('tests/ directory not found')

    return {
        'valid': len(errors) == 0,
        'errors': errors,
        'warnings': warnings,
        'package_name': package_name,
    }

import tempfile
with tempfile.TemporaryDirectory() as tmp:
    os.makedirs(os.path.join(tmp, 'src', 'mypackage'))
    open(os.path.join(tmp, 'pyproject.toml'), 'w').close()
    open(os.path.join(tmp, 'src', 'mypackage', '__init__.py'), 'w').close()
    os.makedirs(os.path.join(tmp, 'tests'))
    print(validate_src_layout(tmp))
Solution
import os

def validate_src_layout(project_root):
errors, warnings = [], []
package_name = None
if not os.path.exists(os.path.join(project_root, 'pyproject.toml')):
errors.append('pyproject.toml not found at project root')
src_dir = os.path.join(project_root, 'src')
if not os.path.isdir(src_dir):
errors.append('src/ directory not found')
else:
packages = [
d for d in os.listdir(src_dir)
if os.path.isdir(os.path.join(src_dir, d))
and not d.startswith('.') and not d.endswith('.egg-info')
]
if not packages:
errors.append('No Python package found inside src/')
else:
package_name = packages[0]
if not os.path.exists(os.path.join(src_dir, package_name, '__init__.py')):
warnings.append('src/' + package_name + '/__init__.py not found')
if not os.path.isdir(os.path.join(project_root, 'tests')):
warnings.append('tests/ directory not found')
return {'valid': len(errors) == 0, 'errors': errors,
'warnings': warnings, 'package_name': package_name}

The src layout solves the "accidental local import" problem. With a flat layout (package at project root), running pytest from the project root puts the project root on sys.path, so import mypackage finds the local source instead of the installed package. This means you are always testing the source tree, never the built artifact. With src layout, the source is not on sys.path by default — you must install the package (pip install -e .) to import it, which means you always test what's actually installed. This is the recommendation for any package that will be published to PyPI.

import os

def validate_src_layout(project_root):
    """Validate that a project follows the src layout convention.
    
    Expected structure:
    project_root/
        pyproject.toml
        src/
            package_name/
                __init__.py
        tests/
    
    Return a dict:
    - 'valid': bool
    - 'errors': list of strings
    - 'warnings': list of strings
    - 'package_name': str or None (detected from src/)
    """
    # TODO: implement
    pass
Expected Output
{'valid': True, 'errors': [], 'warnings': [], 'package_name': 'mypackage'}
Hints

Hint 1: Check os.path.exists for pyproject.toml, src/, and tests/. List src/ contents to find the package dir.

Hint 2: A warning (not error) if tests/ is missing. An error if pyproject.toml is missing.

#8Tool Configuration ExtractorMedium
tool-configTOMLpyproject-consolidation

Extract and summarize all [tool.*] configurations from a pyproject.toml. Modern Python projects consolidate all tool configuration into pyproject.toml — no more setup.cfg, .mypy.ini, .flake8, or pytest.ini cluttering the project root.

Python
def extract_tool_configs(toml_dict):
    tool_section = toml_dict.get('tool', {})
    known = {
        'pytest': 'Test runner configuration',
        'mypy': 'Static type checker',
        'ruff': 'Fast linter and formatter',
        'black': 'Code formatter',
        'isort': 'Import sorter',
        'coverage': 'Test coverage tool',
    }
    tools = sorted(tool_section.keys())
    return {
        'tools': tools,
        'configs': {name: tool_section[name] for name in tools},
        'summary': {name: known.get(name, 'Custom tool configuration') for name in tools},
    }

data = {
    'tool': {
        'pytest': {'ini_options': {'testpaths': ['tests'], 'verbose': True}},
        'mypy': {'strict': True, 'ignore_missing_imports': True},
        'ruff': {'line-length': 88, 'select': ['E', 'F', 'I']},
    }
}
result = extract_tool_configs(data)
print("tools:", result['tools'])
print("summaries:", result['summary'])
Solution
def extract_tool_configs(toml_dict):
tool_section = toml_dict.get('tool', {})
known = {
'pytest': 'Test runner configuration', 'mypy': 'Static type checker',
'ruff': 'Fast linter and formatter', 'black': 'Code formatter',
'isort': 'Import sorter', 'coverage': 'Test coverage tool',
}
tools = sorted(tool_section.keys())
return {
'tools': tools,
'configs': {name: tool_section[name] for name in tools},
'summary': {name: known.get(name, 'Custom tool configuration') for name in tools},
}

Consolidating tool config into pyproject.toml is one of the biggest DX improvements in modern Python. A mature project previously had: setup.py, setup.cfg, MANIFEST.in, tox.ini, pytest.ini, .mypy.ini, .flake8, .isort.cfg, .coveragerc — nine files for one project. Now all of that lives in pyproject.toml under [tool.X] tables. The only tool that cannot use pyproject.toml yet is tox (as of 2024), though support is planned. The ruff linter has effectively replaced flake8 + isort + pyupgrade with a single [tool.ruff] config block.

def extract_tool_configs(toml_dict):
    """Extract all [tool.X] configurations from a
    parsed pyproject.toml dict.
    
    Return a dict:
    - 'tools': list of tool names found
    - 'configs': dict mapping tool_name -> config_dict
    - 'summary': dict mapping tool_name -> one-line description
    
    For summary, use these known descriptions:
    'pytest': 'Test runner configuration',
    'mypy': 'Static type checker',
    'ruff': 'Fast linter and formatter',
    'black': 'Code formatter',
    'isort': 'Import sorter',
    'coverage': 'Test coverage tool',
    For unknown tools, use 'Custom tool configuration'.
    """
    # TODO: implement
    pass
Expected Output
{'tools': ['mypy', 'pytest', 'ruff'], 'configs': {...}, 'summary': {...}}
Hints

Hint 1: Access toml_dict.get("tool", {}) to get all tool configurations.

Hint 2: Sort the tool names for consistent output.


Hard

#9Dynamic Version from GitHard
dynamic-versioninggit-tagsPEP-440

Implement dynamic versioning from git tags. This is what hatch-vcs, setuptools-scm, and versioneer do — derive the package version from git state rather than hard-coding it in pyproject.toml.

The advantage: the version is always in sync with your release tag, and development builds get unique version identifiers.

Python
import subprocess
import re

def get_version_from_git(repo_path='.'):
    try:
        output = subprocess.check_output(
            ['git', 'describe', '--tags', '--long', '--match', 'v[0-9]*'],
            cwd=repo_path,
            stderr=subprocess.DEVNULL,
            text=True,
        ).strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return '0.0.0.dev0'

    # Format: v1.2.3-5-gabc1234
    match = re.match(r'^v?(\d+\.\d+\.\d+)-(\d+)-g([a-f0-9]+)$', output)
    if not match:
        return '0.0.0.dev0'

    base_version, distance, commit_hash = match.group(1), match.group(2), match.group(3)
    if distance == '0':
        return base_version
    return base_version + '.dev' + distance + '+g' + commit_hash

version = get_version_from_git()
print("Version:", version)
Solution
import subprocess
import re

def get_version_from_git(repo_path='.'):
try:
output = subprocess.check_output(
['git', 'describe', '--tags', '--long', '--match', 'v[0-9]*'],
cwd=repo_path, stderr=subprocess.DEVNULL, text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
return '0.0.0.dev0'
match = re.match(r'^v?(\d+\.\d+\.\d+)-(\d+)-g([a-f0-9]+)$', output)
if not match:
return '0.0.0.dev0'
base, distance, ghash = match.group(1), match.group(2), match.group(3)
return base if distance == '0' else base + '.dev' + distance + '+g' + ghash

Dynamic versioning solves the "version bump commit" problem. Without it, every release requires: bump version in pyproject.toml, commit, tag, release — creating a chicken-and-egg situation where the release commit changes files. With dynamic versioning, you just create the tag and the version is automatically correct. The .devN suffix marks pre-release builds as lower than the release version in PEP 440 ordering: 1.2.3.dev5 less than 1.2.3. The +g local version identifier is stripped by PyPI (it does not accept local versions), so CI can build installable wheels from any commit.

import subprocess
import re

def get_version_from_git(repo_path='.'):
    """Derive a PEP 440 version string from git tags.
    
    Logic:
    1. Get the latest git tag matching vX.Y.Z.
    2. Check how many commits since that tag.
    3. If 0 commits since tag: return 'X.Y.Z'
    4. If N > 0 commits: return 'X.Y.Z.devN+gHASH'
    5. If no tags found: return '0.0.0.dev0'
    
    Use subprocess to run git commands.
    Return a version string.
    """
    # TODO: implement
    pass
Expected Output
'1.2.3' or '1.2.3.dev5+gabc1234'
Hints

Hint 1: Run: git describe --tags --long --match "v[0-9]*" to get "v1.2.3-5-gabc1234" format.

Hint 2: Parse the output: split on "-" to get tag, commit count, and hash. Strip the "v" prefix from the tag.

#10Build System Interface SimulatorHard
PEP-517build-backendwheelsdist

Simulate the build backend interface from PEP 517. Understand what happens when python -m build or pip wheel is called — the series of steps that transform source code into an installable artifact.

Python
import os
import re

def normalize_wheel_name(name):
    return re.sub(r'[-_.]+', '_', name).lower()

def read_name_version(project_root):
    cfg_path = os.path.join(project_root, 'pyproject.toml')
    name, version = 'unknown', '0.0.0'
    if not os.path.exists(cfg_path):
        return name, version
    with open(cfg_path) as f:
        content = f.read()
    for line in content.splitlines():
        if re.match(r'\s*name\s*=', line) and name == 'unknown':
            name = line.partition('=')[2].strip().strip('"\'')
        if re.match(r'\s*version\s*=', line) and version == '0.0.0':
            version = line.partition('=')[2].strip().strip('"\'')
    return name, version

def simulate_build_backend(project_root, output_dir, artifact_type='wheel'):
    name, version = read_name_version(project_root)
    wheel_name = normalize_wheel_name(name)

    if artifact_type == 'wheel':
        filename = wheel_name + '-' + version + '-py3-none-any.whl'
        build_steps = [
            'Read pyproject.toml metadata',
            'Validate project structure',
            'Collect source files from src/' + name,
            'Write METADATA to ' + wheel_name + '-' + version + '.dist-info/METADATA',
            'Write WHEEL tag file',
            'Write RECORD checksums',
            'Zip all files into ' + filename,
        ]
        metadata_files = [
            wheel_name + '-' + version + '.dist-info/METADATA',
            wheel_name + '-' + version + '.dist-info/WHEEL',
            wheel_name + '-' + version + '.dist-info/RECORD',
        ]
    else:
        filename = name + '-' + version + '.tar.gz'
        build_steps = [
            'Read pyproject.toml metadata',
            'Collect all non-ignored source files',
            'Generate PKG-INFO from project metadata',
            'Create tar.gz archive as ' + filename,
        ]
        metadata_files = ['PKG-INFO', 'pyproject.toml']

    return {
        'artifact_type': artifact_type,
        'filename': filename,
        'metadata_files': metadata_files,
        'build_steps': build_steps,
    }

import tempfile
with tempfile.TemporaryDirectory() as tmp:
    with open(os.path.join(tmp, 'pyproject.toml'), 'w') as f:
        f.write('[project]\nname = "my-package"\nversion = "1.0.0"\n')
    result = simulate_build_backend(tmp, tmp, 'wheel')
    print(result['filename'])
    print(result['build_steps'][0])
Solution
import os, re

def simulate_build_backend(project_root, output_dir, artifact_type='wheel'):
def read_name_version(root):
cfg = os.path.join(root, 'pyproject.toml')
name, version = 'unknown', '0.0.0'
if os.path.exists(cfg):
with open(cfg) as f:
for line in f:
if re.match(r'\s*name\s*=', line) and name == 'unknown':
name = line.partition('=')[2].strip().strip('"\'')
if re.match(r'\s*version\s*=', line) and version == '0.0.0':
version = line.partition('=')[2].strip().strip('"\'')
return name, version

name, version = read_name_version(project_root)
wn = re.sub(r'[-_.]+', '_', name).lower()
if artifact_type == 'wheel':
fname = wn + '-' + version + '-py3-none-any.whl'
steps = ['Read metadata', 'Collect source', 'Write dist-info', 'Zip wheel']
mfiles = [wn + '-' + version + '.dist-info/' + f for f in ['METADATA', 'WHEEL', 'RECORD']]
else:
fname = name + '-' + version + '.tar.gz'
steps = ['Read metadata', 'Collect sources', 'Generate PKG-INFO', 'Create tar.gz']
mfiles = ['PKG-INFO', 'pyproject.toml']
return {'artifact_type': artifact_type, 'filename': fname,
'metadata_files': mfiles, 'build_steps': steps}

PEP 517 defined the build backend protocol as a set of Python functions that pip calls via subprocess in an isolated environment: build_wheel(wheel_directory, config_settings) and build_sdist(sdist_directory, config_settings). This replaced the old python setup.py bdist_wheel approach which required importing setup.py (and all its side effects) in the build environment. The wheel filename format encodes compatibility: py3-none-any means pure Python 3, no ABI dependency, any platform. A C extension wheel might be cp311-cp311-manylinux_2_17_x86_64.

import os

def simulate_build_backend(project_root, output_dir, artifact_type='wheel'):
    """Simulate what a PEP 517 build backend does.
    
    For artifact_type='wheel':
    - Read pyproject.toml to get name and version
    - Create a minimal wheel filename: name-version-py3-none-any.whl
    - Return a dict describing what would be built
    
    For artifact_type='sdist':
    - Create filename: name-version.tar.gz
    
    Return a dict:
    - 'artifact_type': str
    - 'filename': str
    - 'metadata_files': list of files that would be included in the wheel
    - 'build_steps': list of step description strings
    """
    # TODO: implement
    pass
Expected Output
{'artifact_type': 'wheel', 'filename': 'mypackage-1.0.0-py3-none-any.whl', ...}
Hints

Hint 1: Parse pyproject.toml to get name and version. Normalize name: replace hyphens with underscores for wheel filename.

Hint 2: Wheel filename format: {name}-{version}-{python_tag}-{abi_tag}-{platform_tag}.whl

#11Dependency Conflict Resolution ReportHard
dependency-resolutionconflict-reportpip-internals

Generate a comprehensive dependency conflict report. This is what pip check does — it validates that the installed packages satisfy each other's requirements and reports any inconsistencies.

Python
import re

def parse_dep(req_str):
    req_str = req_str.strip()
    parts = re.split(r'(>=|<=|==|!=|~=|>|<)', req_str, maxsplit=1)
    name = parts[0].strip().lower()
    specifier = None
    if len(parts) > 1:
        specifier = parts[1] + parts[2].strip().split(',')[0]
    return name, specifier

def ver_tuple(v):
    try:
        return tuple(int(x) for x in v.split('.'))
    except ValueError:
        return (0,)

def version_satisfies(version_str, specifier_str):
    ops = {'==': lambda a, b: a == b, '!=': lambda a, b: a != b,
           '>=': lambda a, b: a >= b, '<=': lambda a, b: a <= b,
           '>': lambda a, b: a > b, '<': lambda a, b: a < b}
    for op in ('==', '!=', '>=', '<=', '>', '<'):
        if specifier_str.startswith(op):
            spec_v = specifier_str[len(op):]
            v1 = ver_tuple(version_str)
            v2 = ver_tuple(spec_v)
            maxlen = max(len(v1), len(v2))
            v1 += (0,) * (maxlen - len(v1))
            v2 += (0,) * (maxlen - len(v2))
            return ops[op](v1, v2)
    return True

def generate_conflict_report(installed, project_deps):
    installed_lower = {k.lower(): v for k, v in installed.items()}
    satisfied, conflicts, missing = [], [], []

    for req in project_deps:
        name, specifier = parse_dep(req)
        if name not in installed_lower:
            missing.append(req)
            continue
        inst_ver = installed_lower[name]
        if specifier and not version_satisfies(inst_ver, specifier):
            conflicts.append({
                'req': req,
                'installed_version': inst_ver,
                'reason': 'installed ' + inst_ver + ' does not satisfy ' + specifier,
            })
        else:
            satisfied.append(req)

    return {
        'satisfied': satisfied,
        'conflicts': conflicts,
        'missing': missing,
        'all_ok': len(conflicts) == 0 and len(missing) == 0,
    }

installed = {'requests': '2.28.0', 'flask': '2.0.0', 'click': '8.1.0'}
deps = ['requests>=2.28', 'flask>=2.3.0', 'numpy>=1.24', 'click==8.1.0']
report = generate_conflict_report(installed, deps)
print("satisfied:", report['satisfied'])
print("conflicts:", report['conflicts'])
print("missing:", report['missing'])
print("all_ok:", report['all_ok'])
Solution
import re

def generate_conflict_report(installed, project_deps):
def parse_dep(r):
parts = re.split(r'(>=|<=|==|!=|~=|>|<)', r.strip(), maxsplit=1)
name = parts[0].strip().lower()
spec = (parts[1] + parts[2].strip().split(',')[0]) if len(parts) > 1 else None
return name, spec

def ver_t(v):
try:
return tuple(int(x) for x in v.split('.'))
except ValueError:
return (0,)

def satisfies(version, spec):
ops = {'==': lambda a,b: a==b, '!=': lambda a,b: a!=b,
'>=': lambda a,b: a>=b, '<=': lambda a,b: a<=b,
'>': lambda a,b: a>b, '<': lambda a,b: a<b}
for op in ('==','!=','>=','<=','>','<'):
if spec.startswith(op):
v1, v2 = ver_t(version), ver_t(spec[len(op):])
n = max(len(v1), len(v2))
return ops[op](v1+(0,)*(n-len(v1)), v2+(0,)*(n-len(v2)))
return True

inst = {k.lower(): v for k, v in installed.items()}
satisfied, conflicts, missing = [], [], []
for req in project_deps:
name, spec = parse_dep(req)
if name not in inst:
missing.append(req)
elif spec and not satisfies(inst[name], spec):
conflicts.append({'req': req, 'installed_version': inst[name],
'reason': inst[name] + ' does not satisfy ' + spec})
else:
satisfied.append(req)
return {'satisfied': satisfied, 'conflicts': conflicts,
'missing': missing, 'all_ok': not conflicts and not missing}

pip check runs this exact logic across all installed packages — it reads every .dist-info/METADATA file, extracts Requires-Dist entries, and verifies that each installed package's dependencies are satisfied by what is actually installed. Conflicts are most common after manual pip install commands that upgrade one package without upgrading its dependents. The proper fix is always pip install package1 package2 in a single command — pip's resolver can then find a consistent set, whereas sequential installs may leave the environment in an inconsistent state.

def generate_conflict_report(installed, project_deps):
    """Generate a conflict analysis report.
    
    installed: dict of package_name -> installed_version (strings)
    project_deps: list of requirement strings (name+specifier)
    
    For each project dependency, check if the installed version
    satisfies the specifier. Report:
    - satisfied requirements
    - unsatisfied requirements (conflicts)
    - missing packages
    
    Return a dict:
    - 'satisfied': list of req strings
    - 'conflicts': list of dicts {req, installed_version, reason}
    - 'missing': list of req strings
    - 'all_ok': bool
    """
    # TODO: implement
    pass
Expected Output
{'satisfied': [...], 'conflicts': [...], 'missing': [...], 'all_ok': False}
Hints

Hint 1: Reuse or inline the version_satisfies logic from Problem 2. Parse each dep to get name and specifiers.

Hint 2: A package is missing if its name is not in installed. It conflicts if installed but version fails specifiers.

© 2026 EngineersOfAI. All rights reserved.