Skip to content

Latest commit

 

History

History
460 lines (357 loc) · 19.4 KB

SETUP.md

File metadata and controls

460 lines (357 loc) · 19.4 KB

Setup

Goal: get Python 3.11+, a virtual environment and the three tools running, and know what each command actually checks before you trust its "all clear".

Every file in lessons/, practice/, projects/ and tests/ is held to the same four commands. They are the whole contract:

python -m ruff check .            # lint
python -m ruff format --check .   # formatting
python -m mypy .                  # types, in strict mode
python -m pytest                  # tests

CI runs those four and one thing more: every program under lessons/, practice/ and projects/ is executed, and a single non-zero exit fails the build. make check is all five in one command. Nothing else is required of you, and nothing less is accepted.

You need Minimum Why
Python 3.11 tomllib, except*, typing.Self, better tracebacks - and pyproject.toml says requires-python = ">=3.11"
git any to clone, and .gitattributes normalises line endings for you
a terminal any the whole repository is run from one
make optional convenience only; every target has a plain command underneath

Python 3.12 and 3.13 work too - CI runs all three. Write 3.11 syntax anyway: no PEP 695 type X = ... aliases, no class Box[T]. Use typing.TypeVar and typing.Generic, so what you write runs on the oldest version the repository supports.


1. Install Python

Linux

Distribution Command
Ubuntu 24.04+, Debian 13+ sudo apt update && sudo apt install python3 python3-venv python3-pip
Ubuntu 22.04 (ships 3.10) sudo add-apt-repository ppa:deadsnakes/ppa, then sudo apt install python3.11 python3.11-venv
Fedora sudo dnf install python3.11
Arch sudo pacman -S python
Alpine apk add python3 py3-pip

Debian and Ubuntu split the standard library across packages. Without python3-venv you get ensurepip is not available the first time you build a virtual environment. Install the package; do not work around it.

macOS

Route Command Notes
Homebrew (recommended) brew install python@3.11 puts python3.11 on PATH
python.org installer download the macOS universal2 .pkg installs python3.11 and IDLE
Xcode command line tools xcode-select --install gives you a python3, usually an old one

/usr/bin/python3 belongs to macOS. Leave it alone, and never sudo pip install into it. A virtual environment built from Homebrew's Python keeps your work and the operating system's own tooling apart.

Windows

Route Command
winget (recommended) winget install Python.Python.3.11
python.org installer download, and tick "Add python.exe to PATH" on the first screen
Microsoft Store search "Python 3.11" - fine for learning, awkward for paths

Windows also installs a launcher, py, which is more precise than python:

Command Does
py -0p list every installed Python with its full path
py -3.11 --version run 3.11 specifically, whatever else is installed
py -3.11 -m venv .venv build the virtual environment with 3.11

Any OS: version managers

pyenv (pyenv install 3.11.9 && pyenv local 3.11.9) and uv (uv python install 3.11) both give you an exact version per project without touching the system interpreter. Neither is required here.

Check what you got

python --version        # python3 --version on Linux and macOS
python -c "import sys; print(sys.executable)"
Symptom Meaning Fix
python: command not found Linux and macOS name the binary python3 use python3 - or activate a venv, where python always exists
Windows opens the Store the Store alias is shadowing a real install Settings > Apps > Advanced > App execution aliases, turn off python.exe
it reports 3.9 an older interpreter is first on PATH name the version explicitly when you create the venv: python3.11 or py -3.11

2. Create the virtual environment

pip installs into whichever interpreter runs it. A virtual environment gives this project its own site-packages, so the repository's ruff and mypy cannot be disturbed by another project - and python unambiguously means this project's Python.

git clone https://github.com/limonuser/Python_educator.git
cd Python_educator
python -m venv .venv          # python3.11 -m venv .venv   /   py -3.11 -m venv .venv

That writes .venv/ beside pyproject.toml. It is already in .gitignore: never commit it, never edit inside it, and delete-and-recreate rather than repair it when it breaks.

Activate it

OS and shell Command
Linux / macOS, bash or zsh source .venv/bin/activate
Linux / macOS, fish source .venv/bin/activate.fish
Windows, PowerShell .venv\Scripts\Activate.ps1
Windows, cmd.exe .venv\Scripts\activate.bat
Windows, Git Bash source .venv/Scripts/activate
any of them deactivate to leave

Your prompt gains a (.venv) prefix. Confirm it is real and not cosmetic:

python -c "import sys; print(sys.prefix)"   # ...Python_educator/.venv

Activation lasts for one shell session. A new terminal, a new tab or a restarted editor starts deactivated - which is what "ruff: command not found" means nine times out of ten.

If PowerShell refuses with "running scripts is disabled on this system", that is Windows policy, not a broken install:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

Or skip activation entirely and call the interpreter by path: .venv\Scripts\python.exe -m ruff check . needs no policy change at all.


3. Install the tools

python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
Piece Why it is there
python -m pip runs the pip belonging to this interpreter; a bare pip may belong to another
-e editable install: the project is registered by path, so your edits need no reinstall
. this directory, described by pyproject.toml
[dev] the dev extra: pytest>=8.0, ruff>=0.6, mypy>=1.11
"..." the quotes are for zsh, which treats a bare .[dev] as a glob and refuses to run

Verify:

python -m ruff --version
python -m mypy --version
python -m pytest --version

If the editable install errors

Setuptools looks at the project root for something to package. lessons/, practice/, projects/ and coursework/ all look like candidates, so it refuses to guess:

error: Multiple top-level packages discovered in a flat-layout:
['lessons', 'practice', 'projects', 'coursework'].

(docs/ is missing from that list because setuptools excludes a handful of conventional names - docs, tests, examples, scripts - on its own. The other four it has never heard of.)

That is a packaging question, not a Python one: this repository is a curriculum, not a library, and has nothing to install. Two ways out:

Fix What to do
Tell setuptools there is nothing to package (permanent) add the two tables below to pyproject.toml
Skip the editable install (works right now) python -m pip install "pytest>=8.0" "ruff>=0.6" "mypy>=1.11"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = []      # a curriculum, not a library - there is no package to install

The second route puts the same three tools in the same virtual environment. Nothing under lessons/, practice/ or tests/ imports this project as an installed package, so all four checks behave identically either way.


4. The four commands, and what each one buys you

Command Answers Catches
python -m ruff check . is the code legal, consistent and free of known bug shapes? unused imports, mutable default arguments, os.path where pathlib belongs, unsorted imports
python -m ruff format . is it laid out the one agreed way? rewrites the file: 88 columns, double quotes, spaces
python -m mypy . do the annotations describe what the code really does? None reaching an int, a missing -> None, an unreachable branch
python -m pytest does it still do what it promised? behaviour regressions

They are not interchangeable. ruff check will never notice that a function returns str | None while its caller subscripts the result; mypy will never notice that your quotes are inconsistent; neither notices that the answer is wrong.

ruff check - the linter

Configured under [tool.ruff.lint]. The selected families:

Prefix Family Why it is on
E, W pycodestyle the baseline everyone already expects
F pyflakes unused imports and names, undefined names
I isort import order decided once, not per file
UP pyupgrade stops 3.11 code being written like 2010 code
B bugbear the mutable-default-argument trap lives here
C4 comprehensions list(x for x in y) becomes [x for x in y]
SIM simplify collapses if x: return True / else: return False
RET return inconsistent returns out of one function
PTH pathlib open(os.path.join(...)) becomes Path(...).open()
RUF ruff's own mutable class attributes, useless noqa
Flag Use
--fix apply the safe fixes - then read the diff before committing it
--diff show what --fix would do, and change nothing
--statistics violations counted by rule: the fastest way to see what a file does wrong
--watch re-run on every save
--select B,SIM check those families only, ignoring the configured set
--output-format concise one line per violation, easy to grep

Two rules are off repository-wide. E501 (line too long), because the formatter owns line length and a lint error for something a formatter silently fixes is noise. B007 (unused loop variable), because looping for effect is ordinary in a teaching example. On top of that lessons/** ignores B006, SIM105 and RET504, and practice/** ignores RET504 - those files show the wrong version of a construct before correcting it, and the linter cannot tell the difference.

A justified escape hatch names its rule - # noqa: B008 - never a bare # noqa, which also silences the rules you have not read yet.

ruff format - the formatter

Command Effect
python -m ruff format . rewrite every file in place
python -m ruff format --check . exit 1 if anything would change, touch nothing - this is what CI runs
python -m ruff format --diff . print the diff it would apply

Settings: line length 88, double quotes, space indentation. This is not a matter of taste here - it is what makes a sixty-file diff readable.

ruff check passing does not mean the file is formatted. They are separate commands, and CI runs both.

mypy - the type checker

[tool.mypy] already sets python_version = "3.11", strict = true, warn_unreachable = true and excludes coursework/. So a plain python -m mypy . is strict mode; writing --strict on the command line just says so out loud, which is why every instruction in this repository includes it.

What --strict turns on, and what each part catches:

Sub-flag Rejects
--disallow-untyped-defs a function with no annotations at all
--disallow-incomplete-defs a half-annotated function - the kind that fools readers
--disallow-untyped-calls typed code calling into untyped code
--disallow-any-generics bare list, dict, tuple with the element type unstated
--check-untyped-defs skipping the body of an unannotated function
--warn-return-any returning Any from a function that promised a real type
--warn-unused-ignores a # type: ignore that no longer suppresses anything
--warn-redundant-casts cast(int, x) where x is already int
--no-implicit-reexport importing a name a module imported but never exported
--strict-equality "1" == 1, which is always False and always a mistake

warn_unreachable is layered on top: it flags branches that can never run, which is usually a type narrowing you did not expect.

Flag Use
python -m mypy . the whole repository, as configured
python -m mypy --strict path/to/file.py one file, explicit about the mode
--no-incremental ignore .mypy_cache/ when the errors look impossible
--show-error-codes print [arg-type], the code you would put in # type: ignore[...]
--pretty on already in config: prints the offending source line under each error

To debug a type rather than a value, drop reveal_type(x) into the code, run mypy, read the note, delete the line. It is not a real function: leave it in and Python raises NameError.

pytest - the tests

addopts = "-q --strict-markers" is configured, and testpaths = ["tests"] means a bare pytest collects tests/ and nothing else.

Flag Use
-q one character per test instead of one line (on already)
-x stop at the first failure - the only sane setting while fixing something
-k "median and not empty" run the tests whose names match the expression
--lf re-run only what failed last time
-vv full test names and full assertion diffs
--tb=short shorter tracebacks when the failure is already obvious
-s do not capture stdout - for when you are debugging with print
--co list what would run, run nothing
--strict-markers an unregistered @pytest.mark.foo is an error, not a silent no-op (on already)

5. Running one file, without make

Every program here is standalone: no arguments, no input, exit 0.

You want Command
one lesson cd lessons/09-dicts-and-sets && python mappings.py
one exercise cd practice/beginner && python 01_fizzbuzz.py
one project cd projects/01-calculator && python calc.py
a file, from the repository root python lessons/09-dicts-and-sets/mappings.py
the exit code echo $? in bash, echo $LASTEXITCODE in PowerShell
to lint one file python -m ruff check lessons/09-dicts-and-sets/mappings.py
to type-check one file python -m mypy --strict lessons/09-dicts-and-sets/mappings.py

cd first is what the Makefile and CI both do, and it is the habit worth keeping. It is not load-bearing here: every program in this repository that writes a scratch file anchors it on Path(__file__).resolve().parent, so the file lands beside the program however you launched it. That is deliberate, and it is the pattern to copy. A bare Path("out.txt") is relative to the current directory instead, which is how a lesson ends up littering the repository root.

Two files deserve a note of their own:

File How to run it Why
lessons/14-modules-and-packages/main.py from either directory it imports the neighbouring textkit/ package, and the lesson explains what sys.path did to allow that
lessons/15-files-and-context-managers/fileio.py from anywhere it creates _scratch/ beside itself and removes it in a finally

6. make, and what to type instead

Target Equivalent command
make install python -m pip install -e ".[dev]"
make lint python -m ruff check .
make format python -m ruff format .
make format-check python -m ruff format --check .
make types python -m mypy .
make test python -m pytest
make run run every file under lessons/, practice/ and projects/, requiring exit 0
make lesson-09 cd lessons/09-dicts-and-sets && python mappings.py
make check lint, format-check, types, test, run - exactly what CI does
make clean remove __pycache__/, the three tool caches and the generated demo files

Windows has no make by default. Use Git Bash or WSL, install one (winget install GnuWin32.Make, or choco install make), or just type the right-hand column: nothing in this repository depends on make existing.


7. What CI enforces

.github/workflows/ci.yml runs on every push to main, every pull request against it, and on demand from the Actions tab (workflow_dispatch). The matrix is Python 3.11, 3.12 and 3.13, in parallel, with fail-fast: false so one version breaking still tells you about the other two:

Step Command
1 python -m pip install -e ".[dev]"
2 python -m ruff check .
3 python -m ruff format --check .
4 python -m mypy .
5 python -m pytest
6 run every lesson, exercise and project; a single non-zero exit fails the build

A second job runs every file in coursework/ on 3.13 and asks only that each one exits 0. Those are the author's original class exercises, kept verbatim: extend-exclude in [tool.ruff] and exclude in [tool.mypy] hold the linters off them, but nothing excuses them from running.

make check is the first list, locally. Run it before you push and CI holds no surprises.


The traps

  • A new terminal is a deactivated terminal. "ruff: command not found" and "but I installed it yesterday" are the same sentence. Re-activate, or call .venv/bin/python -m ruff by path.
  • pip install without python -m can target a different interpreter. The package lands somewhere real - just not where your python will look for it.
  • pip install -e .[dev] unquoted fails in zsh with no matches found: .[dev], because zsh expands the brackets before pip ever sees them.
  • ruff check clean does not mean formatted. CI runs ruff format --check as a separate step and it fails on its own.
  • ruff check --fix edits your files. Run --diff first when you are unsure what a rule wants, and read the change before you commit it.
  • mypy one_file.py is not mypy .. A single file can miss errors that only appear across module boundaries, and can report ones the full run resolves.
  • A stale .mypy_cache/ produces impossible errors after you move or rename files. make clean, or --no-incremental, before you go hunting.
  • Path("out.txt") is relative to the current directory, not to the source file. Run such a program from the repository root and the file lands in the root. Every program here sidesteps it by anchoring on Path(__file__).resolve().parent; do the same in yours.
  • Never sudo pip install. It writes into the interpreter your operating system depends on, and a virtual environment exists so that you never have to.
  • Do not commit .venv/. Machine-specific, hundreds of megabytes, already ignored. If it shows up in git status, something overrode .gitignore.
  • Line endings are settled by .gitattributes - LF in the repository, native in your working copy. Do not fight it with a local core.autocrlf override; that is how a one-line change becomes a four-hundred-line diff.

Run it

From nothing to a green checkout:

git clone https://github.com/limonuser/Python_educator.git
cd Python_educator

python -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\Activate.ps1

python -m pip install --upgrade pip
python -m pip install -e ".[dev]"    # see section 3 if this errors

python -m ruff check .
python -m ruff format --check .
python -m mypy .
python -m pytest

cd lessons/01-hello-world && python hello.py

Next: Cheat sheet - Back to: the curriculum