FreeCampus Python

Build a Project You Can Navigate and Share

Arrange a clear src-layout project, remove working-directory assumptions, ignore generated files, and record understandable changes with essential Git commands.
python-foundations modules-environments-projects project-layout git
Open in Colab
  • Level: Python Foundations
  • Estimated time: 4–5 hours
  • You will learn: Give source, checks, documentation, configuration, and generated output predictable homes; explain why a src layout requires installation; and inspect, stage, commit, ignore, and branch in a disposable Git repository.
  • Practice in: A local editor and terminal using a disposable project

Imagine opening a collaborator’s folder and finding Python files, virtual environments, exported reports, notes, and old copies mixed together. Which file starts the program? Which code may callers import? Which output can be deleted? Which changes will the next Git commit contain? A project layout answers those questions before a person has to open every file.

You will arrange the constellation package and record three small snapshots. Keep these questions nearby:

1. Give every project responsibility a visible home

Build toward this tree:

constellation-report/
├── .gitignore
├── README.md
├── pyproject.toml
├── src/
│   └── constellation/
│       ├── __init__.py
│       ├── __main__.py
│       ├── angles.py
│       └── reports.py
└── checks/
    └── check_public_api.py

Each part answers a different question:

Path Responsibility
pyproject.toml standardized build and project metadata plus tool configuration
README.md human-facing purpose and first-use instructions
src/constellation/ importable product source
checks/ executable assertions that verify visible behavior
.gitignore local/generated paths Git should normally leave untracked
.git/ Git’s private repository database, created by git init

Do not create folders merely because a template has them. Add docs/, data/, or notebooks/ when the project truly owns those responsibilities. A clear small tree is better than a large empty architecture.

Create the starting directories from the project root:

mkdir -p src/constellation checks
touch src/constellation/__init__.py

On Windows PowerShell, use New-Item -ItemType Directory and New-Item or create the paths in the editor. The tree is the contract; shell spelling can vary by platform.

Source belongs under one import-package directory

# src/constellation/angles.py
FULL_CIRCLE = 360


def normalize_degrees(value):
    """Return an angle in the range 0 <= result < 360."""
    return value % FULL_CIRCLE
# src/constellation/reports.py
from .angles import normalize_degrees


def observation_line(label, raw_angle):
    """Return one normalized constellation line."""
    angle = normalize_degrees(raw_angle)
    return f"{label.strip().title()}: {angle}°"
# src/constellation/__init__.py
from .angles import normalize_degrees
from .reports import observation_line

__all__ = ["normalize_degrees", "observation_line"]

src is not part of the import name. After installation, callers use import constellation, not import src.constellation.

The repository contains source and project support files. Installation makes the package importable in an environment; callers do not import through the repository folder name.

flowchart LR
  repository["Repository"] --> source["src/constellation"]
  repository --> checks["checks/"]
  repository --> metadata["pyproject.toml"]
  metadata --> install["Installation step"]
  source --> install
  install --> environment["Environment site-packages"]
  environment --> caller["import constellation"]

Checkpoint: classify the tree

2. Let the src layout expose accidental imports

With package source directly at the repository root, launching Python there can make that source importable even if the project was never installed. A src layout removes that convenient coincidence.

Before installation, this command from constellation-report/ should fail:

python -c "import constellation"

That failure is useful. It prevents a check from passing against source files that a built distribution might forget to include. Lesson 6 will add an editable installation during development and a clean wheel installation for final proof.

Do not repair the failure this way:

# Avoid in project source.
import sys

sys.path.insert(0, "src")

The hard-coded path depends on the current working directory and lets every entry script invent a different import configuration. Install the project into the intended environment instead.

Keep resource and configuration roots explicit

Avoid guessing a repository root by walking a fixed number of parents from __file__:

# Brittle: package depth and installation layout are not configuration contracts.
from pathlib import Path

guessed_repository = Path(__file__).resolve().parents[2]

After wheel installation, that path points inside the environment, not back to the user’s source checkout. Choose among these contracts instead:

  • caller-supplied input/output paths;
  • current-working-directory paths when the command explicitly documents that interface;
  • installed package resources accessed with importlib.resources; or
  • configuration supplied through the later command/configuration layer.

For a function that writes a report, pass the destination:

from pathlib import Path


def save_report(text, destination):
    """Write report text to an explicit destination and return that path."""
    path = Path(destination)
    path.write_text(text, encoding="utf-8")
    return path

The caller owns location policy. The package owns how to write the text.

Verify behavior from a temporary destination

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as temporary_name:
    output = Path(temporary_name) / "night-report.txt"
    saved = save_report("Vega: 1°\n", output)
    assert saved == output
    assert output.read_text(encoding="utf-8") == "Vega: 1°\n"

This check supplies its dependency and cleans up its artifact. It works no matter where the notebook itself started.

3. Ignore artifacts that can be recreated locally

Python, environments, build tools, editors, and operating systems create local files. Record project source and deliberate configuration; normally ignore reproducible caches and outputs:

# Virtual environments
.venv/

# Python caches and installed-project metadata
__pycache__/
*.py[cod]
*.egg-info/

# Build outputs
build/
dist/

# Tool caches
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Local editor and operating-system files
.DS_Store

Do not blindly ignore all data, configuration, notebooks, or lock files. Ask whether the repository deliberately owns the file and whether collaborators need the same content. A generated report normally differs from a hand-curated example fixture.

After adding .gitignore, inspect ignored paths:

git status --short --ignored
git check-ignore -v .venv/ dist/example.whl

git check-ignore -v reports the exact ignore rule and file that matched. If a file was already tracked, adding it to .gitignore does not erase it from history or automatically stop tracking it. Ignore rules primarily affect untracked paths.

Checkpoint: remove hidden location assumptions

4. Git records snapshots, not a cloud folder

Git is a version-control system. A repository stores a graph of snapshots and the relationships between them. Three everyday views matter first:

  1. the working tree contains files you are currently editing;
  2. the staging area records the exact content proposed for the next commit;
  3. the current commit is the checked-out recorded snapshot.

Editing, staging, and committing are separate actions. Inspection can happen at every boundary.

flowchart LR
  commit["Current commit"] -->|"edit files"| working["Working tree changes"]
  working -->|"git add selected paths"| staged["Staged proposal"]
  staged -->|"git commit"| next["New commit"]
  working -.->|"git diff"| inspect1["Inspect unstaged changes"]
  staged -.->|"git diff --staged"| inspect2["Inspect proposed snapshot"]

Initialize only inside the disposable constellation-report folder:

git init
git status

Before the first commit, configure an author identity if Git asks. You may set it for this repository rather than changing every repository:

git config user.name "Course Learner"
git config user.email "learner@example.invalid"

Use your real project identity outside a disposable exercise.

Inspect before staging

git status --short
git diff

For untracked files, git diff has no earlier tracked content to compare. Read the files and status list. Stage only the coherent first snapshot:

git add .gitignore README.md pyproject.toml src checks
git status --short
git diff --staged

In short status, the first column describes the index/staging state; the second describes the working tree. For example, M means staged modification, M means unstaged modification, and ?? means untracked.

Commit only after the staged diff contains what the message claims:

git commit -m "Create constellation package skeleton"
git log --oneline --decorate -n 3

A commit is local. It does not automatically upload anywhere. Remotes, hosting, and collaboration workflows build on these local concepts.

5. Stage a file twice when it changes twice

Staging copies the file’s current content into the proposed snapshot. If you edit the working file again, the staged version and working version differ.

Try this controlled sequence:

printf "# Constellation Report\n" > README.md
git add README.md
printf "\nBuild night-sky observation reports.\n" >> README.md
git status --short
git diff
git diff --staged

The staged diff contains the title. The unstaged diff contains the later description relative to the staged version. Run git add README.md again only if both lines belong in the next commit.

This distinction prevents “I staged it earlier” from becoming accidental loss of later work. Always review git diff --staged immediately before committing.

Unstage a mistaken path without deleting the work

Suppose git diff --staged reveals that notes/private-scratch.txt entered the proposal accidentally. Unstage it while leaving the working file available:

git restore --staged notes/private-scratch.txt
git status --short
git diff --staged

--staged changes the index proposal. It does not mean “delete my working file.” This is different from:

# Read before using: this discards unstaged working-tree edits to README.md.
git restore README.md

Never paste a restore command merely because status looks untidy. First decide which copy—current commit, staged proposal, or working edit—you intend to keep. When the work matters, copy it outside the experiment or make a deliberate snapshot before practicing destructive recovery.

For a new untracked file, git restore has no committed version to recover. Untracked work needs its own deletion or preservation decision. git clean can delete many untracked files and is intentionally outside this beginner lab.

Translate short status one path at a time

Use this sequence whenever the two columns are confusing:

git status --short
git diff -- README.md
git diff --staged -- README.md

Then fill a small table:

Question Answer for README.md
Is a version tracked in the current commit? inspect status/history
What content is staged? inspect git diff --staged -- README.md
What later content is only in the working tree? inspect git diff -- README.md
What should the next commit claim? compare both diffs with the intended change

Path-limited diffs reduce noise without changing any state. Once the answer is clear, stage again, unstage, or keep editing. Inspection commands are safe to repeat.

Write commits around one understandable change

Useful beginner commits might be:

  1. Create constellation package skeleton
  2. Add normalized observation report
  3. Document local verification command

Avoid one commit that mixes a feature, unrelated formatting, generated build artifacts, and experimental notes. Small coherent snapshots make history useful for explanation and recovery.

6. Branches let names point at different lines of work

A branch is a movable name for a commit, not a duplicate folder by definition. Create a short-lived branch before trying a report-format change:

git switch -c report-header
git branch --show-current

Edit and commit the focused change, then inspect the graph:

git log --oneline --decorate --graph --all -n 8

Switching branches updates tracked working-tree files to the selected snapshot. Git may refuse when uncommitted changes would be overwritten. Read that refusal instead of forcing it. Merging, resolving conflicts, rebasing, and remote branch collaboration deserve later guided practice; this lesson’s boundary is creating, identifying, and inspecting a branch without losing work.

Read history as a project explanation

git log --oneline abbreviates identifiers and subjects for navigation. Ask Git for one complete snapshot when you need its actual change:

git show --stat HEAD
git show --format=fuller --no-ext-diff HEAD

The first summarizes affected paths. The second displays commit metadata and patch. A message such as Update files explains almost nothing; Add normalized observation report lets a reader predict what the patch should contain.

Commit identifiers are derived from commit content and relationships, not sequence numbers such as “commit 3.” Two learners following the same lesson can receive different identifiers because author, timestamp, or content differs. Refer to an identifier or branch name when discussing a particular snapshot.

Before switching away from report-header, run:

git status --short
git log --oneline --decorate --graph --all -n 8

If status is not clean, decide whether the work belongs in a coherent commit or should remain unfinished. Do not assume branch switching stores arbitrary working changes as a new snapshot; commits create the history you can later explain.

7. Record a three-snapshot project story

Complete the constellation tree and create these snapshots:

Snapshot 1: the package skeleton

  • .gitignore
  • README.md with project purpose
  • src/constellation/__init__.py
  • placeholder pyproject.toml comment if Lesson 6 metadata is not written yet

Snapshot 2: observable behavior

Add angles.py, reports.py, and this built-in assertion script:

# checks/check_public_api.py
from constellation import normalize_degrees, observation_line

assert normalize_degrees(-1) == 359
assert observation_line(" vega ", 361) == "Vega: 1°"
print("public API checks passed")

The import will work after Lesson 6 installs the project. Until then, record the expected failure rather than adding src to sys.path.

Snapshot 3: command boundary

# src/constellation/__main__.py
from .reports import observation_line


def main():
    """Display one example constellation observation."""
    print(observation_line("lyra", -15))


if __name__ == "__main__":
    main()

For every snapshot:

  1. run git status --short;
  2. inspect the relevant files and git diff;
  3. stage named paths;
  4. inspect git diff --staged;
  5. commit with a message describing the one change; and
  6. inspect git log --oneline --decorate.

Then create report-header, add a header in the report output, and keep the branch unmerged. Explain which commit each branch points to.

Compare the evidence expected from one clean history
* <newest> (HEAD -> report-header) Add night report header
* <third>  (main) Add package execution boundary
* <second> Add normalized observation report
* <first>  Create constellation package skeleton

Exact hashes differ because each commit identifier depends on its content and metadata. The important evidence is the parent sequence, the two branch labels, and clean git status --short output after each committed snapshot. dist/, .venv/, caches, and *.egg-info/ should not enter the staged diff.

Checkpoint: inspect before recording

Audit the project as a new collaborator

Close files and begin at the repository root. Without relying on editor tabs, answer these questions from the tree and Git:

git status --short --ignored
git log --oneline --decorate --graph --all -n 8
git ls-files
  • Can you identify importable product source without opening checks or build output?
  • Can you name the supported import package without including src?
  • Does git ls-files contain only deliberate source, configuration, documentation, and checks?
  • Are environment, cache, installed-metadata, and build paths either absent or reported as ignored?
  • Does each commit subject match its staged patch?
  • Which commit do main and report-header currently name?

Use Python to classify tracked paths by their first component:

import subprocess
from collections import Counter
from pathlib import Path

result = subprocess.run(
    ["git", "ls-files"],
    text=True,
    capture_output=True,
    check=True,
)
tracked = [Path(line) for line in result.stdout.splitlines()]
groups = Counter(path.parts[0] for path in tracked if path.parts)
print(groups)

The counts are descriptive rather than pass/fail rules. An unexpected .venv, dist, or cache group deserves investigation; an unfamiliar source folder may be a valid project decision. Explain every top-level group in one sentence.

Finally, clone-like confidence requires more than a clean status. In Lesson 6, a new environment will install declared source and run it away from the repository. This audit establishes that the repository contains the inputs for that proof rather than machine history masquerading as source.

Do the audit again from a plain terminal, not only an editor’s source-control panel. Graphical tools are useful, but the command output creates portable evidence another learner can compare. Record the project root and branch name with that output so a clean status from the wrong repository cannot be mistaken for the intended proof. Keep that compact audit with the handoff notes for later comparison.

8. Key points for navigable, versioned projects

  • A useful tree separates importable source, checks, human documentation, project metadata, and generated/local artifacts.
  • In a src layout, src is not part of the import name. Installation makes the package importable and exposes accidental repository-root imports.
  • Pass paths or configuration through explicit interfaces; do not infer a repository by walking a fixed number of parents from __file__.
  • .gitignore keeps reproducible local artifacts out of untracked-path reports; it does not automatically untrack existing content.
  • Git distinguishes working-tree content, staged content, and committed snapshots. Inspect each state before moving to the next.
  • git add stages current content, so edit-after-stage creates both staged and unstaged changes.
  • A commit is a local snapshot with a parent relationship. A branch is a movable name pointing into that history.

References

Back to top