# Python Development Windows Native Setup
Adding Python development on top of a Windows 11 machine already configured per [[General_Development_Windows_Native_Setup]]. Most of the stack carries over — Windows Terminal, PowerShell 7, Starship, Git, GitHub CLI, VS Code, Claude Code, and the PSReadLine setup all work identically. This guide only covers the Python-specific additions. It's the Windows-native counterpart to [[Python_Development_Mac_Tahoe_Setup]] and [[Python_Development_Ubuntu_Setup]], and everything here installs the same way on **ARM64 (Snapdragon, Surface Pro 11, Dev Kit 2023) and x64 (Intel/AMD)** — with one important ARM64 caveat for `uv python install` covered below.
> [!tip] AI assistance is optional
> The reference to Claude Code above assumes you completed Part 7 of the General guide. If you skipped it (which is fine) or use a different AI tool (Copilot, Cursor, Continue, etc.), this guide's instructions still work — the language toolchain doesn't depend on any AI being present.
> [!info] Coexistence
> C, Python, Ruby, Go, and Rust can all live on the same Windows system without interfering. Each language's toolchain installs to its own prefix: `uv` under `%LOCALAPPDATA%\uv\` and `%USERPROFILE%\.local\bin\`, Rust under `%USERPROFILE%\.rustup\` and `%USERPROFILE%\.cargo\`, Go under `%USERPROFILE%\go\`, Ruby under its own MSYS2-managed tree, MSVC under Visual Studio's prefix. No conflicts.
---
## Why `uv`?
`uv` is Astral's unified Python toolchain. It replaces **pyenv-win + venv + pip + pip-tools + pipx** with a single fast binary and has essentially become the default for new Python projects in 2026. Written in Rust, 10–100× faster than pip for most operations.
| Use case | Pre-2024 tool(s) | With uv |
|----------|------------------|---------|
| Install a Python version | pyenv-win | `uv python install 3.13` |
| Create a virtualenv | `python -m venv` | `uv venv` (auto on `uv init`) |
| Install dependencies | pip + requirements.txt | `uv add <pkg>` → `pyproject.toml` + lockfile |
| Global CLI tools | pipx | `uv tool install <pkg>` |
| Lock dependencies | pip-tools | built-in (`uv.lock`) |
| Run scripts | `python foo.py` | `uv run foo.py` |
> [!info] Why not the python.org installer or the Microsoft Store Python?
> Both will work, but they each have rough edges on Windows. The python.org installer doesn't manage multiple versions cleanly — you end up juggling PATH order between 3.12 and 3.13. The Microsoft Store Python is sandboxed in a way that confuses pip and some IDEs. `uv` sidesteps all of this by managing its own Python builds in a private prefix — nothing you do touches a system interpreter, and switching versions per-project is a single line in `.python-version`.
---
## Install uv
`uv` ships native binaries for both Windows ARM64 and x64. The official PowerShell installer detects your architecture automatically:
```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
Or, equivalently, via winget:
```powershell
winget install --id=astral-sh.uv -e
```
Both methods install to `%USERPROFILE%\.local\bin\uv.exe` (already on your PATH from the General guide, which prepends `$HOME\.local\bin`). Open a new PowerShell window or reload your profile with `. $PROFILE`, then verify:
```powershell
uv --version
```
### Install Python versions
`uv` manages Python installations independently of anything Windows already had. Multiple versions coexist:
```powershell
uv python install 3.13
uv python install 3.12
uv python install 3.11
uv python list
```
These install to `%LOCALAPPDATA%\uv\python\` — nothing touches the python.org installer's directories or the Microsoft Store Python. The downloads are Astral's `python-build-standalone` builds.
> [!warning] ARM64: force a native aarch64 build
> On **Windows ARM64 hosts**, `uv python install 3.13` historically resolves to an **x86_64 CPython** binary, which then runs under Windows' x64 emulation. This works but adds startup overhead and prevents linking against native ARM64 wheels. To force a native aarch64 install:
>
> ```powershell
> uv python install 3.13 --python-platform aarch64-pc-windows-msvc
> ```
>
> Confirm the architecture after install:
>
> ```powershell
> uv run python -c "import platform; print(platform.machine())"
> # Should print: ARM64 (not AMD64 — that would mean you got the x64 build under emulation)
> ```
>
> See <https://pydevtools.com/handbook/how-to/how-to-use-uv-on-windows-arm64/> for background. On x64 hosts no flag is needed — the default download is correct.
### Global Python CLI tools
`uv tool` is the `pipx` replacement. Install commonly-used CLIs globally:
```powershell
uv tool install ruff
uv tool install ipython
uv tool install mypy
```
These land in `%USERPROFILE%\.local\bin\` (already on your PATH from the General guide's `$PROFILE` block) and are callable by name anywhere.
---
## VS Code extensions for Python
```powershell
# Core Python support — language server, debugger, formatter integration
code --install-extension ms-python.python
code --install-extension ms-python.vscode-pylance
code --install-extension ms-python.debugpy
# Ruff (linter + formatter, matches the CLI you installed with uv)
code --install-extension charliermarsh.ruff
# Jupyter notebooks (optional)
code --install-extension ms-toolsai.jupyter
code --install-extension ms-toolsai.jupyter-renderers
```
---
## VS Code settings
Append these language-specific settings to your existing `settings.json` at `%APPDATA%\Code\User\settings.json`. The general settings from [[General_Development_Windows_Native_Setup]] stay in place; these merge with them:
```json
{
// ── Python ────────────────────────────────────────────────
"python.defaultInterpreterPath": ".venv\\Scripts\\python.exe",
"python.terminal.activateEnvInCurrentTerminal": true,
"python.terminal.activateEnvironment": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.analysis.inlayHints.functionReturnTypes": true,
"python.analysis.inlayHints.variableTypes": false,
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.tabSize": 4,
"editor.rulers": [88, 120],
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"ruff.nativeServer": "on",
"ruff.lineLength": 88,
// Hide Python clutter from the file explorer
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
"**/.pytest_cache": true,
"**/.ruff_cache": true,
"**/.mypy_cache": true
},
"search.exclude": {
"**/.venv": true,
"**/uv.lock": true
}
}
```
> [!info] `python.defaultInterpreterPath`
> Pointing at `.venv\Scripts\python.exe` (note the Windows-style path — `Scripts\python.exe`, **not** `bin/python` like on macOS/Linux) means any project made with `uv init` "just works" — VS Code finds the venv, activates it in new terminals, and uses it for Pylance type-checking. No manual interpreter selection per-project.
> [!info] Why double backslashes in the JSON?
> JSON treats `\` as an escape character, so a literal backslash in a Windows path has to be written `\\`. Forward slashes also work (`.venv/Scripts/python.exe`) and many people prefer them for readability; either form is correct.
---
## Full demo: A calculator program
This walks through building, testing, debugging, and publishing a four-function calculator from scratch. Parallel implementations exist in C, Ruby, Go, and Rust.
### Create the project
```powershell
cd $HOME\projects
uv init calc-python --python 3.13
cd calc-python
```
This creates:
```
calc-python\
├── .python-version
├── .gitignore
├── README.md
├── main.py
└── pyproject.toml
```
Remove the auto-generated `main.py` — we'll replace it with a proper project layout.
```powershell
Remove-Item main.py
New-Item -ItemType Directory -Path src\calc, tests -Force | Out-Null
```
### `src\calc\__init__.py` — the core logic
```python
"""Four-function calculator."""
from __future__ import annotations
def add(a: float, b: float) -> float:
return a + b
def sub(a: float, b: float) -> float:
return a - b
def mul(a: float, b: float) -> float:
return a * b
def div(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("division by zero")
return a / b
def calculate(a: float, op: str, b: float) -> float:
"""Dispatch to the appropriate operation based on op."""
match op:
case "+":
return add(a, b)
case "-":
return sub(a, b)
case "*" | "x":
return mul(a, b)
case "/":
return div(a, b)
case _:
raise ValueError(f"unknown operator {op!r}")
```
### `src\calc\__main__.py` — the CLI entry point
```python
"""Command-line entry point for the calculator."""
from __future__ import annotations
import sys
from calc import calculate
def usage(prog: str) -> None:
print(f"Usage: {prog} <number> <op> <number>", file=sys.stderr)
print(" op: + - * /", file=sys.stderr)
print(f"Example: {prog} 6 + 7", file=sys.stderr)
def main(argv: list[str] | None = None) -> int:
argv = argv if argv is not None else sys.argv
if len(argv) != 4:
usage(argv[0])
return 1
try:
a = float(argv[1])
b = float(argv[3])
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
try:
result = calculate(a, argv[2], b)
except (ValueError, ZeroDivisionError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
# Print as integer if it came out whole
print(int(result) if result == int(result) else result)
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
### `tests\test_calc.py` — unit tests with pytest
```python
"""Tests for the four-function calculator."""
from __future__ import annotations
import pytest
from calc import calculate
class TestCalculate:
def test_add(self):
assert calculate(6, "+", 7) == 13
def test_sub(self):
assert calculate(10, "-", 3) == 7
def test_mul_star(self):
assert calculate(4, "*", 5) == 20
def test_mul_x(self):
assert calculate(4, "x", 5) == 20
def test_div(self):
assert calculate(20, "/", 4) == 5
def test_div_by_zero(self):
with pytest.raises(ZeroDivisionError):
calculate(5, "/", 0)
def test_unknown_operator(self):
with pytest.raises(ValueError, match="unknown operator"):
calculate(1, "?", 2)
@pytest.mark.parametrize(
"a, op, b, expected",
[
(1, "+", 1, 2),
(0, "+", 0, 0),
(-1, "+", 1, 0),
(1.5, "+", 2.5, 4.0),
(10, "-", 20, -10),
(3, "*", 4, 12),
(1, "/", 3, pytest.approx(0.3333, rel=1e-3)),
],
)
def test_operations_parametrized(a, op, b, expected):
assert calculate(a, op, b) == expected
```
### Update `pyproject.toml`
Replace the generated `pyproject.toml` with:
```toml
[project]
name = "calc-python"
version = "0.1.0"
description = "Four-function command-line calculator"
requires-python = ">=3.13"
dependencies = []
[project.scripts]
calc = "calc.__main__:main"
[dependency-groups]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"mypy>=1.10",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/calc"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v --cov=calc --cov-report=term-missing"
[tool.ruff]
line-length = 88
target-version = "py313"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "SIM"]
```
> [!info] Forward slashes in `pyproject.toml`
> Even on Windows, `packages = ["src/calc"]` uses forward slashes — this is a TOML/Python convention, not a filesystem path. Hatchling translates it correctly on every platform.
### Install dev dependencies
```powershell
uv sync --all-groups
```
This creates `.venv\`, installs `pytest`, `pytest-cov`, `mypy`, and the project itself in editable mode.
> [!tip] Activating the venv (rarely needed)
> `uv run` runs commands inside the project's venv without activation, which is the modern workflow and what this guide uses throughout. If you ever want to activate it explicitly in PowerShell (e.g., to run `python` interactively):
>
> ```powershell
> . .venv\Scripts\Activate.ps1
> ```
>
> Note the leading dot — that *sources* the script into your current session. Without it, the script runs in a child process and the activation evaporates the moment it exits. To deactivate: `deactivate`.
### Run everything
```powershell
# Run the CLI
uv run calc 6 + 7
uv run calc 10 - 3
uv run calc 4 x 5
uv run calc 20 / 4
# Or run the module directly
uv run python -m calc 6 + 7
# Run tests
uv run pytest
# Type check
uv run mypy src/
# Lint and format (using the global ruff)
ruff check src/ tests/
ruff format src/ tests/
# Or fix issues automatically
ruff check --fix src/ tests/
```
> [!info] `*` is not a wildcard in PowerShell command arguments
> On Linux/Mac shells, `uv run calc 4 * 5` would have the shell expand `*` to filenames in the current directory before the program ever sees it. PowerShell **doesn't** do glob expansion on argv by default — programs receive the literal `*`. The calculator's `4 * 5` example works in PowerShell exactly as written. (If you ever notice argv expansion happening, it's because the program you're calling is doing its own globbing.)
### Debug in VS Code
Create `.vscode\launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug calc CLI",
"type": "debugpy",
"request": "launch",
"module": "calc",
"args": ["6", "+", "7"],
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${workspaceFolder}/src"
}
},
{
"name": "Debug pytest",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": ["-v"],
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${workspaceFolder}/src"
}
}
]
}
```
> [!info] `${workspaceFolder}` and forward slashes
> Even on Windows, VS Code's `launch.json` accepts forward slashes inside `${workspaceFolder}/src` — VS Code normalizes them. Use them for portability across machines.
### Publish to GitHub
Same workflow as any project (see [[General_Development_Windows_Native_Setup#Publishing a repo to GitHub]]):
```powershell
git init
git add .
git commit -m "Initial commit: four-function calculator"
gh repo create calc-python --private --source=. --remote=origin --push
```
### `CLAUDE.md` for this project
```markdown
# Project: calc-python
## Purpose
Four-function command-line calculator in Python — pedagogical example.
## Conventions
- Python 3.13 via uv (.python-version pins it)
- src/ layout with package `calc`
- Tests in tests/ with pytest
- Format and lint with Ruff (config in pyproject.toml)
- Type hints on all public functions
## Commands
- Install deps: `uv sync --all-groups`
- Run: `uv run calc <a> <op> <b>`
- Test: `uv run pytest`
- Test with coverage: `uv run pytest --cov`
- Type check: `uv run mypy src/`
- Format: `ruff format src/ tests/`
- Lint: `ruff check --fix src/ tests/`
## Style
- 4-space indent, 88-column line length (Black/Ruff default)
- PEP 8 naming: snake_case functions/variables, CamelCase classes
- Type hints required on public APIs
- `from __future__ import annotations` at top of every module
```
---
## Full demo: A calculator GUI
Same calculator, wrapped in a graphical interface. This uses **Tkinter**, Python's standard GUI toolkit.
### Tkinter on Windows
uv's `python-build-standalone` interpreters bundle Tk on both architectures, so **if you installed Python via `uv python install` (above), `import tkinter` just works** — nothing extra to install.
> [!warning] python.org's ARM64 installer does NOT bundle Tk
> If you instead installed Python from python.org's **ARM64** installer rather than uv, you'll hit `ModuleNotFoundError: No module named '_tkinter'` the moment you import `tkinter`. python.org's ARM64 builds intentionally ship without Tk. Three ways out:
>
> 1. **Recommended — switch to uv-managed Python** for this project: `uv python install 3.13 --python-platform aarch64-pc-windows-msvc` and re-run `uv sync`. uv's bundled Python includes Tk.
> 2. **Use the x64 python.org build**, which does include Tk. It runs under Windows' transparent x64 emulation on ARM64 — slower, but functional for a GUI demo.
> 3. **Install Tk via conda-forge** (`conda install -c conda-forge tk python`) if you already use Miniforge/Mambaforge.
>
> The x64 python.org installer does include Tk, so on **x64 hosts** all three install paths (uv, python.org, Microsoft Store) work for Tkinter. The ARM64-specific gotcha only affects python.org's ARM64 build.
> [!info] Why Tkinter?
> It ships with Python (via uv on Windows), zero extra dependencies, works on all platforms, and renders acceptably with the native Windows theme. For a pedagogical calculator it's the right default. Alternatives (PySide6, Flet) are noted at the end.
### Add the GUI module
Create `src\calc\gui.py`:
```python
"""Tkinter GUI for the four-function calculator."""
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
from calc import calculate
class CalculatorApp:
"""A simple four-function calculator GUI."""
BUTTON_GRID = [
["7", "8", "9", "/"],
["4", "5", "6", "*"],
["1", "2", "3", "-"],
["0", ".", "=", "+"],
]
def __init__(self, root: tk.Tk) -> None:
self.root = root
self.root.title("Calculator")
self.root.resizable(False, False)
# Internal state
self.current = ""
self.stored: float | None = None
self.pending_op: str | None = None
self._build_ui()
self._bind_keys()
def _build_ui(self) -> None:
style = ttk.Style()
style.configure("Calc.TButton", font=("Segoe UI", 16), padding=10)
style.configure("CalcOp.TButton", font=("Segoe UI", 16, "bold"), padding=10)
# Display
self.display_var = tk.StringVar(value="0")
display = ttk.Entry(
self.root,
textvariable=self.display_var,
justify="right",
font=("Cascadia Mono", 24),
state="readonly",
)
display.grid(row=0, column=0, columnspan=4, sticky="ew", padx=8, pady=8)
# Button grid
for row_idx, row in enumerate(self.BUTTON_GRID, start=1):
for col_idx, label in enumerate(row):
style_name = (
"CalcOp.TButton"
if label in {"+", "-", "*", "/", "="}
else "Calc.TButton"
)
btn = ttk.Button(
self.root,
text=label,
style=style_name,
command=lambda lbl=label: self._on_press(lbl),
)
btn.grid(row=row_idx, column=col_idx, sticky="nsew", padx=2, pady=2)
# Clear button spans the bottom
clear_btn = ttk.Button(self.root, text="Clear", command=self._clear)
clear_btn.grid(row=5, column=0, columnspan=4, sticky="ew", padx=8, pady=8)
# Make columns expand evenly
for col in range(4):
self.root.columnconfigure(col, weight=1, minsize=60)
def _bind_keys(self) -> None:
for key in "0123456789.":
self.root.bind(key, lambda e, k=key: self._on_press(k))
for key in ["+", "-", "*", "/"]:
self.root.bind(key, lambda e, k=key: self._on_press(k))
self.root.bind("<Return>", lambda e: self._on_press("="))
self.root.bind("=", lambda e: self._on_press("="))
self.root.bind("<Escape>", lambda e: self._clear())
self.root.bind("<BackSpace>", lambda e: self._backspace())
def _on_press(self, label: str) -> None:
if label.isdigit() or label == ".":
self.current += label
self.display_var.set(self.current or "0")
elif label in {"+", "-", "*", "/"}:
self._apply_pending()
self.pending_op = label
self.current = ""
elif label == "=":
self._apply_pending()
self.pending_op = None
def _apply_pending(self) -> None:
if not self.current:
return
try:
value = float(self.current)
except ValueError:
self.display_var.set("Error")
self._reset_state()
return
if self.stored is None or self.pending_op is None:
self.stored = value
else:
try:
self.stored = calculate(self.stored, self.pending_op, value)
except (ZeroDivisionError, ValueError):
self.display_var.set("Error")
self._reset_state()
return
# Display whole numbers as integers
display_val = int(self.stored) if self.stored == int(self.stored) else self.stored
self.display_var.set(str(display_val))
self.current = ""
def _reset_state(self) -> None:
self.current = ""
self.stored = None
self.pending_op = None
def _clear(self) -> None:
self._reset_state()
self.display_var.set("0")
def _backspace(self) -> None:
self.current = self.current[:-1]
self.display_var.set(self.current or "0")
def main() -> None:
root = tk.Tk()
CalculatorApp(root)
root.mainloop()
if __name__ == "__main__":
main()
```
> [!info] Segoe UI and Cascadia Mono
> The fonts are swapped from the Linux version's `DejaVu` family to Windows defaults — **Segoe UI** for the button labels (the Windows system font, present on every Windows install) and **Cascadia Mono** for the display (Microsoft's modern monospace, shipped with Windows Terminal and present on Windows 11 by default). Tk will fall back gracefully on systems missing these fonts.
### Add a GUI entry point to `pyproject.toml`
```toml
[project.scripts]
calc = "calc.__main__:main"
calc-gui = "calc.gui:main"
```
Re-sync to pick up the new script:
```powershell
uv sync --all-groups
```
### Tests for the GUI state machine
GUI event handling is hard to test directly, but the state machine (`_apply_pending`, `_on_press`) is pure logic and easily unit-tested. Create `tests\test_gui.py`:
```python
"""Tests for the calculator GUI's state machine."""
from __future__ import annotations
import tkinter as tk
import pytest
from calc.gui import CalculatorApp
@pytest.fixture
def app():
"""Create a CalculatorApp with a real but invisible Tk root."""
root = tk.Tk()
root.withdraw()
app = CalculatorApp(root)
yield app
root.destroy()
class TestCalculatorState:
def test_initial_display(self, app):
assert app.display_var.get() == "0"
def test_single_digit(self, app):
app._on_press("5")
assert app.display_var.get() == "5"
def test_multi_digit(self, app):
for ch in "123":
app._on_press(ch)
assert app.display_var.get() == "123"
def test_simple_addition(self, app):
app._on_press("6")
app._on_press("+")
app._on_press("7")
app._on_press("=")
assert app.display_var.get() == "13"
def test_chained_operations(self, app):
# 2 + 3 * 4 evaluated left-to-right → (2+3)*4 = 20
for ch in ["2", "+", "3", "*", "4", "="]:
app._on_press(ch)
assert app.display_var.get() == "20"
def test_division(self, app):
for ch in ["2", "0", "/", "4", "="]:
app._on_press(ch)
assert app.display_var.get() == "5"
def test_division_by_zero_shows_error(self, app):
for ch in ["5", "/", "0", "="]:
app._on_press(ch)
assert app.display_var.get() == "Error"
def test_clear_resets_state(self, app):
for ch in ["5", "+", "3"]:
app._on_press(ch)
app._clear()
assert app.display_var.get() == "0"
assert app.current == ""
assert app.stored is None
assert app.pending_op is None
def test_decimal_input(self, app):
for ch in "1.5":
app._on_press(ch)
app._on_press("+")
for ch in "2.5":
app._on_press(ch)
app._on_press("=")
assert app.display_var.get() == "4"
```
### Run the GUI
```powershell
uv run calc-gui
```
A small calculator window opens. Click buttons or use the keyboard — digits, `+`, `-`, `*`, `/`, `Enter` (for `=`), `Esc` (to clear), and `Backspace` (to delete the last digit).
### Run all tests (CLI + GUI)
```powershell
uv run pytest
```
The `CalculatorApp` tests use a hidden Tk root (`root.withdraw()`) so no windows pop up during test runs.
> [!info] Headless testing on Windows
> Unlike Linux (where you need `xvfb` for headless GUI tests), Windows' graphics stack provides a default display context for every desktop session, including over RDP. Tk tests run cleanly in any normal user session. For unattended CI on a headless Windows runner without an interactive session, GitHub Actions' `windows-latest` image already has Tk working under the runner's session — no special configuration needed. The `root.withdraw()` call keeps test windows from flashing on screen.
---
## Alternative GUI libraries
If Tkinter's default look bothers you, here are two drop-in alternatives:
### PySide6 (Qt for Python, native-looking widgets)
```powershell
uv add pyside6
```
Good for polished applications. Ships with Qt Designer for drag-and-drop UI layout. Larger install (~200 MB). Wheels available for both Windows ARM64 and x64.
### Flet (Flutter-powered, modern declarative UI)
```powershell
uv add flet
```
Write UIs in pure Python with Flutter widgets. Produces web, desktop, and mobile targets from the same code.
For pedagogical work, Tkinter's zero-dependency "it just works" wins.
---
## Starship prompt — Python auto-detection
The general Starship config you set up already includes the `[python]` module. When you `cd` into this project, the prompt shows:
```
~\projects\calc-python main 3.13.2 (calc-python) ❯
```
Reading left-to-right: directory, git branch, Python version, active venv name.
---
## direnv — auto-activate this project's environment
direnv can load this project's `.venv` automatically when you `cd` in, so a bare `python`, `pytest`, or `ruff` uses it without the `uv run` prefix (see [[General_Development_Windows_Native_Setup]] for the PowerShell hook).
```bash
# from the project root, after `uv sync` has created .venv\
cat > .envrc <<'EOF'
export VIRTUAL_ENV="$PWD/.venv"
PATH_add "$VIRTUAL_ENV/Scripts"
EOF
direnv allow
```
> [!warning] Two things to get right on Windows
> 1. **`.envrc` is bash.** direnv runs it through bash and applies the result to PowerShell, so write bash syntax and keep Git for Windows (Git Bash) on `PATH`. Forward slashes are fine — but note the venv lives in `.venv/Scripts` on Windows, not `.venv/bin`.
> 2. **Create the venv first.** `uv init` doesn't create `.venv\` — only `uv sync` (or the first `uv run`) does. Run `uv sync` before `direnv allow`, or `python` won't resolve to the venv.
`uv run` already uses `.venv` regardless of direnv; this just extends that to tools you invoke directly.
---
## Troubleshooting
> [!warning] `python: command not found` after installing via uv
> `uv` doesn't put a `python` shim on your PATH globally — it uses `uv run python` inside projects. For a global `python` command, either add a function to `$PROFILE`:
>
> ```powershell
> function python { uv run python @args }
> ```
>
> or use `uv python install 3.13 --default`, which installs a shim at `%USERPROFILE%\.local\bin\python.exe`.
> [!warning] `ModuleNotFoundError: No module named 'tkinter'` on ARM64
> You're using a Python that didn't ship with Tk — most likely the python.org ARM64 installer. Switch to uv-managed Python: `uv python install 3.13 --python-platform aarch64-pc-windows-msvc`, then `uv sync` to rebuild the venv. See the Tkinter section above for the alternatives.
> [!warning] `uv python install 3.13` gives me x86_64 on an ARM64 machine
> This is the known ARM64 default-resolution issue. Force the native build with `uv python install 3.13 --python-platform aarch64-pc-windows-msvc`. Confirm with `uv run python -c "import platform; print(platform.machine())"` — it should print `ARM64`.
> [!warning] VS Code doesn't find the `.venv\`
> Open the workspace from a terminal (`code .` from Windows Terminal), not from File Explorer — VS Code inherits the shell's environment. Or manually select the interpreter: `Ctrl+Shift+P` → "Python: Select Interpreter" → browse to `.venv\Scripts\python.exe`.
> [!warning] `ModuleNotFoundError: No module named 'calc'` when running tests
> The `src\` layout requires the package path to be set. The provided `pyproject.toml` includes `pythonpath = ["src"]` under `[tool.pytest.ini_options]` — make sure this line is present.
> [!warning] `Activate.ps1` won't run — "running scripts is disabled on this system"
> PowerShell's default execution policy blocks unsigned local scripts. Either use `uv run` (which doesn't need activation), or relax the policy for your user only:
>
> ```powershell
> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
> ```
>
> This allows local scripts and signed remote scripts; it's the standard developer setting on Windows.
> [!warning] `pip install` inside an activated venv suddenly hangs or is slow
> Don't use `pip` directly — `uv pip install` is a drop-in replacement that's 10–100× faster and uses the same wheel index. Better still, use `uv add` to update `pyproject.toml` and the lockfile in one step.
> [!warning] Long path errors on `uv sync` (`The filename or extension is too long`)
> Windows' historical 260-character path limit can bite deep dependency trees. Enable long paths:
>
> ```powershell
> # Elevated PowerShell, one time per machine
> New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
> -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
> ```
>
> Plus enable `git config --global core.longpaths true`. Reboot after.
> [!warning] `Defender` slows down `uv sync` on first install
> Windows Defender real-time scanning hits hard when uv unpacks hundreds of wheels. If you trust the path, add an exclusion in Windows Security → Virus & threat protection → Manage settings → Exclusions: `%LOCALAPPDATA%\uv\` and your `%USERPROFILE%\projects\` folder. Typical speedup: 2–4×.
---
## Summary — the one-shot Python addition
> [!warning] This is a checklist, not a script
> Copy and paste one block at a time. One step in the middle is a **manual file-paste step** (append the Python settings block to `%APPDATA%\Code\User\settings.json`) — do that before the verify step at the end will reflect your editor config.
```powershell
# Python toolchain (architecture-aware installer)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# ...or, equivalently:
# winget install --id=astral-sh.uv -e
# Reload PowerShell profile (or open a new tab)
. $PROFILE
# Install Python versions
# On x64:
uv python install 3.13 3.12
# On ARM64, force the native aarch64 build:
# uv python install 3.13 --python-platform aarch64-pc-windows-msvc
# uv python install 3.12 --python-platform aarch64-pc-windows-msvc
# Global CLI tools
uv tool install ruff
uv tool install ipython
uv tool install mypy
# (Tk libraries ship with uv-managed Python on both architectures — nothing to install)
# VS Code extensions
code --install-extension ms-python.python
code --install-extension ms-python.vscode-pylance
code --install-extension ms-python.debugpy
code --install-extension charliermarsh.ruff
code --install-extension ms-toolsai.jupyter
# Paste the Python block into %APPDATA%\Code\User\settings.json
# Verify
uv --version; uv python list; ruff --version
```
About three minutes on top of the general setup.
---
## Related notes
- [[General_Development_Windows_Native_Setup]]
- [[Python_Development_Ubuntu_Setup]] — the Linux counterpart
- [[Python_Development_Mac_Tahoe_Setup]] — the macOS counterpart
- [[C_Development_Windows_Native_Setup]]
- [[Ruby_Development_Windows_Native_Setup]]
- [[Go_Development_Windows_Native_Setup]]
- [[Rust_Development_Windows_Native_Setup]]
- [uv Quick Reference](https://docs.astral.sh/uv/)
- [uv on Windows ARM64 (PyDevTools)](https://pydevtools.com/handbook/how-to/how-to-use-uv-on-windows-arm64/)
- [pytest Patterns](https://docs.pytest.org/en/stable/how-to/index.html)