Common Questions

This page answers common questions about the decisions used in this guide.

The goal is not to cover every possible Python workflow.

The goal is to explain why this repository uses a small, practical setup for learning modern Python project structure.

Why not just use pip?

pip is an important Python tool, and many projects still use it.

However, this guide uses uv because it provides a compact workflow for:

  • creating project environments,
  • managing dependencies,
  • locking dependency versions,
  • running tools inside the project environment,
  • working with Python versions.

For beginners moving from scripts to projects, using one consistent tool can make the workflow easier to understand.

This does not mean pip is bad.

It means this guide chooses uv to keep the project workflow modern and reproducible.

Why commit uv.lock?

The uv.lock file stores the exact dependency versions resolved for the project.

It should be committed because it helps make the project reproducible.

Without a lockfile, different machines may install different dependency versions.

That can lead to confusing situations where:

  • tests pass on one machine but fail on another,
  • CI uses different versions than local development,
  • a dependency update breaks the project unexpectedly.

The pyproject.toml file describes dependency requirements.

The uv.lock file records the exact resolved versions.

Both files are important.

Why use a src/ layout?

This guide uses:

src/text_toolkit/

instead of placing the package directly in the repository root.

The src/ layout makes the project structure clearer:

  • source code lives in src/,
  • tests live in tests/,
  • documentation lives in docs/,
  • GitHub-specific files live in .github/.

It also avoids some import confusion that can happen when Python accidentally imports code directly from the current working directory.

In this guide, src/ is used to teach clean project structure.

Building and publishing distributions are separate topics.

Is this a complete Python packaging guide?

No.

This guide uses a build system, installs the package from src/, and builds standard distribution files. It does not cover the full publishing workflow.

It does not currently explain publishing to PyPI.

That is intentional.

The first version focuses on:

  • project structure,
  • dependency management,
  • tests,
  • linting,
  • formatting,
  • CI,
  • pull requests.

Publishing can be added later as a separate topic.

See Building distributions for the wheel and source distribution workflow.

Why does pytest not use pythonpath = ["src"]?

The pytest configuration includes:

[tool.pytest.ini_options]
testpaths = ["tests"]

The project has a [build-system], so uv sync installs it into the project environment in editable mode.

For example:

from text_toolkit import count_words

This verifies that imports work through the installed project instead of a pytest-only path modification. Editable installation means that source changes are still available immediately during development.

Why use Ruff?

Ruff provides linting and formatting in one fast tool.

In this guide, Ruff helps check:

  • unused imports,
  • import ordering,
  • common style issues,
  • suspicious patterns,
  • opportunities to use modern Python syntax.

It also formats code consistently.

The goal is not to satisfy tools blindly.

The goal is to reduce repetitive review comments and keep code easier to maintain.

Why not enable all Ruff rules?

Ruff supports many rules.

Enabling everything at once can be overwhelming, especially in a beginner-friendly guide.

This project starts with a moderate rule set:

select = [
    "E",
    "F",
    "I",
    "B",
    "UP",
]

More rules can be added later if the project needs them.

A smaller configuration that people understand is better than a strict setup copied without context.

Is it okay to ignore some Ruff rules?

Yes, if the decision is intentional.

Some rules may be too strict for specific contexts, such as:

  • tests,
  • UI code,
  • educational examples,
  • visual demos,
  • configuration-heavy code.

For example, strict configurations may complain about magic values in tests.

A small test like this can still be readable:

def test_count_words_handles_repeated_whitespace() -> None:
    text = "Python    project\nworkflow"

    result = count_words(text)

    assert result == 3

The value 3 is clear in this context.

If a rule is too strict only for tests, prefer a narrow exception:

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
    "PLR2004",
]

The important thing is to know why a rule is disabled.

Why use rumdl instead of formatting every Markdown file?

rumdl can report structural and consistency problems without rewriting the documentation by default.

That suits this guide because prose layout is part of the teaching material. A fully opinionated formatter could create a large diff without improving the meaning of the text.

The project disables the noisy line-length rule and keeps two narrow per-file exceptions for intentional examples. All other default checks remain active.

The local link test and human review still have separate responsibilities. Linting cannot confirm that an explanation is correct or useful.

Why not add Docker?

Docker is useful in many projects.

However, it would add another layer of tooling to this guide.

This repository focuses on the Python project foundation first:

  • project structure,
  • dependencies,
  • tests,
  • linting,
  • formatting,
  • CI,
  • pull requests.

Docker can be introduced later when there is a clear reason for it.

Adding it too early would make the guide heavier without helping the main learning path.

Why was pre-commit added after the foundation?

pre-commit is useful for running checks before commits.

It also adds another tool and another configuration file.

This guide first teaches the commands directly:

uv run ruff check .
uv run ruff format --check .
uv run rumdl check .
uv run mkdocs build --strict
uv run pytest --cov=text_toolkit --cov-report=term-missing
uv run mypy

The guide therefore introduces hooks only after these commands are clear.

The hooks automate fast file checks and Ruff. They do not hide or replace the complete local checks and CI workflow.

Why was type checking added after the foundation?

Type checking with tools such as Pyright or mypy can be valuable.

The example code used type hints before the guide introduced mypy.

That is intentional.

Before adding type checking, the guide first explains:

  • project structure,
  • dependency management,
  • tests,
  • linting,
  • formatting,
  • CI.

Readers can now learn those foundations first and then continue to the type-checking chapter. This order keeps type hints and the tool that checks them from becoming another unexplained layer.

Why was coverage added after the testing basics?

Coverage tools can show which lines are executed by tests.

However, coverage can be misleading if introduced too early.

High coverage does not automatically mean good tests.

This guide first focuses on writing clear tests that check behavior. It then introduces coverage as a diagnostic tool in the coverage chapter.

The project deliberately has no percentage gate. Readers should inspect missing behavior instead of treating a high number as proof of test quality.

Why does CI use uv sync --locked --group dev?

Local development usually uses:

uv sync

CI uses:

uv sync --locked --group dev

The --locked option tells uv to use the existing uv.lock file without updating it. The --group dev option explicitly installs the development tools used by the quality checks.

This matters because CI should verify the committed project state.

CI should not silently change the lockfile.

If pyproject.toml and uv.lock are out of sync, CI should fail and tell the developer to update the lockfile locally.

What should I run after cloning the repository?

Run:

uv sync

Then run the local checks:

uv run ruff check .
uv run ruff format --check .
uv run rumdl check .
uv run mkdocs build --strict
uv run pytest --cov=text_toolkit --cov-report=term-missing
uv run mypy

If formatting fails, run:

uv run ruff format .

Then repeat the checks.

What should I commit?

Commit files that describe the project and its source code, such as:

pyproject.toml
uv.lock
.python-version
src/
tests/
docs/
.github/
README.md
CONTRIBUTING.md

Do not commit generated local files such as:

.venv/
__pycache__/
.pytest_cache/
.ruff_cache/
.mypy_cache/
.rumdl_cache/
.coverage
site/

These files are local artifacts and should stay ignored.

Is this repository a template?

Not exactly.

This repository can inspire a project template, but its main purpose is educational.

It explains why each file and tool exists.

The goal is not only to copy the final structure.

The goal is to understand how the structure grows step by step.

Why keep both README.md and docs/index.md?

The root README.md introduces the repository to people browsing its source on GitHub. docs/index.md introduces the generated documentation site.

They point readers toward the same material, but they appear in different contexts. Keeping the site landing page inside docs/ also follows MkDocs' normal source layout.

Why does pull request CI build the site without publishing it?

A strict build verifies configuration, navigation, links, anchors, and rendering without changing external state.

Publishing requires a separate deployment workflow, permissions, and GitHub Pages settings. In this repository, that workflow runs only after relevant changes reach main, so a pull request cannot update the public site.

Where is the documentation site published?

The site is available at https://michalmaj.github.io/modern-python-project-guide/.

The Documentation workflow builds it from the same Markdown sources and deploys a generated artifact. The generated site/ directory is not committed.

Rule of thumb

If a tool helps you understand, run, test, or maintain the project, it may belong here.

If a tool adds complexity before the basics are clear, it should probably wait.