Cursor on a Python project: the setup that doesn't fight you
Published 2026-05-11 by Owner
Fresh Cursor install on a Python project, and within five minutes it’s suggesting match syntax for a service that runs on 3.9, importing from the system Python instead of the project venv, and generating functions without a single type annotation. None of these are bugs in Cursor — they’re defaults that made sense for the median project, not yours. Three small configuration steps and one habit change fix all of them.
Interpreter selection: tell Cursor which Python is yours
Cursor inherits VS Code’s Python extension for interpreter selection. By default it picks the first Python it finds on PATH, which is usually the system Python or whatever brew installed last. That’s rarely the version your project targets — and the mismatch matters. If your project runs on 3.10 but Cursor’s interpreter is 3.12, it will cheerfully suggest match/case syntax, ExceptionGroup, and tomllib (standard library in 3.11+) without flagging any of these as version-specific. The suggestions are correct for 3.12; they just don’t run on what you’re shipping.
The fix takes 30 seconds. Open the command palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Windows/Linux) and run Python: Select Interpreter. You’ll see a list of every Python Cursor found. For a project using .venv:
Python 3.12.3 64-bit ('.venv': venv) ← this one
Python 3.11.5 64-bit ('/usr/local/bin')
Python 3.9.7 64-bit ('/usr/bin')
Pick the one that says ('.venv': venv) or ('.venv': poetry) or similar. Cursor writes this choice to .vscode/settings.json:
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
}
Commit that file. Every team member who opens the project gets the right interpreter without doing anything. It also means Cursor’s linting, import resolution, and autocomplete all run against the same Python that your CI does, which removes a whole class of “it works for me” disagreements.
If the .venv entry isn’t in the list, the extension hasn’t discovered it yet. Click Enter interpreter path and type .venv/bin/python manually. Cursor will add it and remember it. On Windows the path is .venv\Scripts\python.exe — the extension accepts both slash styles.
Venv discovery: conda, poetry, and .venv
For most projects, picking the interpreter once is enough. A few situations need extra attention:
conda environments don’t always live inside the project directory. If your env is in ~/miniconda3/envs/myproject, the interpreter path is ~/miniconda3/envs/myproject/bin/python. Select it with Python: Select Interpreter → Enter interpreter path. Conda envs also work via Python: Select Interpreter directly if the conda extension is installed.
poetry creates its venvs in a cache directory by default (~/.cache/pypoetry/virtualenvs/). The venv path shows up in the interpreter list once you’ve run poetry install. If it doesn’t, run poetry env info --path in the terminal to get the exact path, then paste it into the interpreter selector.
.venv not found on build CI is a separate problem from Cursor’s, but worth noting: if .venv is in .gitignore (it should be), make sure CI recreates it via python -m venv .venv && pip install -r requirements.txt (or equivalent) before running any tooling.
The setting that tells Cursor where to look for virtual environments:
{
"python.venvPath": "${workspaceFolder}",
"python.venvFolders": [".venv", "venv", "env"]
}
Add this to .vscode/settings.json so the Python extension scans the workspace root first. The venvFolders array covers the three most common names; add ".tox" if the project uses tox for test environments, or "env" if an older convention is in use.
.cursor/rules for Python style
Once the interpreter is right, the next problem is style. Cursor will generate Python that works but looks like a random Stack Overflow answer: mixed type annotation coverage, bare except clauses, mutable default arguments, inconsistent string quoting. A .cursor/rules/python.mdc file fixes this.
Create .cursor/rules/python.mdc:
---
description: Python conventions for this project
globs: **/*.py
alwaysApply: false
---
# Type annotations
- All function signatures must include parameter types and return type.
- Use `list[str]` not `List[str]` (Python 3.9+ generics in lowercase).
- Use `X | None` not `Optional[X]` (Python 3.10+ union syntax).
- For dict types: `dict[str, Any]` not `Dict[str, Any]`.
# Patterns to avoid
- Never use mutable defaults: no `def f(x=[])` or `def f(x={})`. Use `None` and assign inside.
- No bare `except:` — always `except SomeSpecificError:`.
- No `from module import *`.
- No `print()` for debugging in committed code. Use the `logging` module.
# asyncio
- Async functions must be called with `await`. Never mix sync and async without a bridge.
- Use `asyncio.TaskGroup` for concurrent tasks (Python 3.11+), not bare `asyncio.gather`.
# Imports
- Standard library, then third-party, then local. One blank line between groups.
- Prefer absolute imports over relative (`from myproject.utils import x`).
The “Patterns to avoid” section is the part that earns its keep. Cursor is good at following positive instructions (“use this pattern”) but even better at avoiding things when you name them explicitly. Mutable default arguments are the most common footgun Cursor will silently introduce if you don’t forbid them. The pattern def process(items=[]) is syntactically valid and Cursor knows it — the only reason not to suggest it is if you tell Cursor it’s forbidden in your project.
The asyncio section is worth adding once a project has any async code. Cursor will write valid async patterns, but “valid” ranges from asyncio.gather with raw coroutines to asyncio.TaskGroup with structured concurrency. If the project already uses one pattern, the rule keeps Cursor consistent with it instead of mixing styles across files.
Keep the rule under 50 lines. Longer rules get internally summarized and the specific guidance gets lost. If the rule is growing past that, split it by domain: one .mdc for type annotations, one for async patterns, with appropriate globs on each.
pytest integration: running tests from Cursor
Cursor surfaces pytest through the VS Code Testing sidebar. For it to work, the Python extension needs to know you’re using pytest and where your tests live. Without this configuration, the Testing sidebar shows nothing, and the only way to run a test is to open a terminal and type pytest by hand — which works, but loses the tight feedback loop that makes test-driven work in Cursor fast.
.vscode/settings.json:
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests"],
"python.testing.unittestEnabled": false
}
After saving this, open the Testing sidebar (the flask icon, or Cmd+Shift+T / Ctrl+Shift+T). Cursor will discover all test files matching test_*.py or *_test.py under tests/. Each test function appears as a separate entry with a run button.
The keyboard shortcuts worth memorizing:
Cmd+;thenA— run all testsCmd+;thenF— run tests in the current fileCmd+;thenC— run the test at the cursor position
The last one (Cmd+;, C) is the one that changes how fast you iterate. Write a function, write the test, put the cursor in the test body, hit Cmd+;, C. The test runs without leaving the editor.
For projects using pytest-cov, add --cov to pytestArgs:
{
"python.testing.pytestArgs": ["tests", "--cov=src", "--cov-report=term-missing"]
}
Coverage output appears in the Cursor terminal pane after each test run. The term-missing reporter shows which specific lines weren’t covered, which is more useful than a bare percentage when deciding what to test next.
The habit that changes everything: type hints everywhere, always
The setup steps above are mechanical. This one is behavioral, and it matters more.
Cursor generates better Python code when your existing codebase has dense type annotations than when it doesn’t. The reason is direct: when Cursor reads the files around your cursor position to build context, typed functions tell it exactly what types flow in and out of each function. Untyped functions leave it guessing.
The difference shows up in Cursor’s suggestions for a new function that calls two existing ones. With full type annotations on both callers, Cursor produces the right parameter types, the right return type, and a signature that matches the rest of the file. Without annotations, it produces something that works but may use Any or leave return types off entirely — which then propagates into every function that calls the new one.
The practical approach: when Cursor generates a new function, always add type annotations before accepting. Even if the suggestion is otherwise perfect, annotating it before committing takes five seconds and means the next function Cursor writes in that file has better context to work from. Over a codebase of a few hundred functions, this compounds. A codebase with 80% annotation coverage gets meaningfully better Cursor suggestions than one at 30%.
Run mypy --strict (or pyright --strict) as a pre-commit check. Not because the CI will catch runtime errors — it won’t for type issues — but because it enforces that the annotations are accurate rather than decorative. An inaccurate annotation is worse than no annotation; it gives Cursor (and human readers) a wrong model of the code.
If you’re adding types to a legacy codebase, start with the files you edit most often. Cursor’s suggestions improve file by file. Full coverage isn’t required to see the effect — the files with types get better suggestions immediately, regardless of what the rest of the codebase looks like.
The four steps together — correct interpreter, venv pointing, Python rules, pytest wired up — take about fifteen minutes on a new project. After that, Cursor stops generating List[str] imports from typing, stops dropping functions in the wrong Python version’s style, and stops ignoring the test file sitting three directories up. The type hints habit is slower to build but has the highest return: it makes every Cursor interaction in that codebase better, not just the obvious ones.