Once you can read Python comfortably and have a code editor with an assistant, it’s time to start your own project and set up the tools that keep a model’s quality high.

By applying the structure described here right from the start, you make sure that anything you build stays maintainable and can scale. This matters most when you share your model with others or plan to use it for a long time.

Your assistant will create most of this setup for you, often in a single request. You don’t need to write these files yourself, but you do need to know what each one is for and what its settings do, because together they decide how your model is installed, checked and versioned, and how well the assistant can work in it. This page explains the purpose of each part and what to check.

Managing your Project

Starting a new project begins with an empty folder named after the project (e.g., “FinanceToolkit”), opened in your editor, with Git initialized (git init) and connected to a remote repository (git remote add origin URL). You can ask your assistant to do all of this.

Every project of mine contains the files below. A good request is to point the assistant to an existing project you trust, such as the Finance Toolkit repository, and ask it to set up the same files for your new project. Then check each one against the descriptions here. This applies to both personal and professional projects.

This consists of the following files:

  • README.md: Includes a (short) description of the repository with useful links, similar to the file you are currently reading.
  • .gitignore: Includes Python-specific and project-specific exclusions (e.g., .idea, .vscode, .venv, .pytest_cache, .DS_Store). See an example here.
  • pyproject.toml: Includes build setups, linter configurations, and dependencies (example). This file is central to the project, containing all necessary information to run it. It succeeds the older setup.py and setup.cfg files. Read more about it in PEP 518.
  • .pre-commit-config.yaml: A configuration file for pre-commit. It specifies which linters should run before code is committed, which helps keep the code quality high (example). Activate it by running pre-commit install in the terminal.

These files are there to make the model last. For example, if you are still using append in Pandas, you haven’t kept up with package dependencies, as this functionality was deprecated in v1.4.0 (January 2022). Failing to keep up with these developments means that without code changes, you’ll be stuck on older versions like Pandas v1.4.0 and consequently, older Python versions like 3.8 and 3.9.

Directory Structure

The folder structure could look like the following, with emphasis on the financetoolkit folder containing the actual financial models:

Folder structure of the financetoolkit package showing its module layout

The tests folder mirrors this structure but contains unit tests for pytest:

Folder structure of the tests directory mirroring the financetoolkit package

You can modify this structure, as long as the model is organized in a way that you and others can understand and maintain.

For instance, my FinanceToolkit package follows this exact structure. You’ll find a similar structure in other projects such as OpenBB, a major open-source finance project. This structure can also be expanded, for example, by adding _view modules.

In any case, using the structure described above clarifies the location and purpose of each file, as discussed further in Structure your Model. A predictable structure also helps AI assistants: when tests mirror the model and every module has a clear name, an assistant (and a colleague) knows where new code and its test belong.

Dependency Management

Your model is much more likely to keep working over the next few years if you keep its dependencies up to date. This involves defining the minimum required versions of Python, Pandas, NumPy, SciPy, and other libraries for the model to function correctly. It would look something like this in the pyproject.toml file:

[project]
requires-python = ">=3.10, <3.13"
dependencies = [
    "pandas[computation,performance,plot]>=2.1.0",
    "requests>=2.31.0",
    "scikit-learn>=1.3.1",
]

[dependency-groups]
dev = [
    "pytest>=7.4.1",
    "codespell>=2.2.5",
    "black>=23.7.0",
    "pytest-mock>=3.11.1",
    "pytest-recording>=0.13.0",
    "pytest-cov>=4.1.0",
    "ruff>=0.0.287",
    "pytest-timeout>=2.1.0",
    "pytest-recorder>=0.2.3",
    "ipykernel>=6.25.2",
]

This example uses the dependency manager uv, a fast tool written in Rust that handles dependency resolution, virtual environment creation, and package installation. It is much faster than alternatives like pip or Poetry and supports the standard pyproject.toml format. The uv documentation is well written and worth reading, see here.

In practice your assistant runs these commands when it needs a new package, so review what it adds: every dependency is code you rely on and have to keep up to date. Once set up, you can add dependencies using uv add (e.g., uv add pandas). This command adds the package (like pandas) to the [project] dependencies section. With dependencies listed, running uv sync installs them all within a virtual environment that uv manages automatically. This command also creates a uv.lock file, which records the exact versions of all installed packages and their sub-dependencies. Because the versions are locked, builds are reproducible.


Pro-tip: Keeping track of new versions for every package can be tedious, so consider using extras as defined with Pandas. These optional dependency groups bundle related packages (like those needed for data wrangling), simplifying maintenance. See here.


Additionally, the [dependency-groups] section lists dependencies relevant only for development. As a user running the model, you only need the core dependencies listed under [project] dependencies. As a developer, you need these additional tools for tasks like running tests, linting, etc.

Why uv over requirements.txt or pip?
uv is a proper dependency manager that understands the relationships between packages. A requirements.txt only lists packages and doesn’t prevent incompatible installations, whereas uv resolves the dependencies, and does so quickly thanks to its Rust-based implementation. Pip is a package installer, not a dependency manager, making it less suitable for maintaining consistent environments. While alternatives like Poetry and pipenv exist, I prefer uv because of its speed, its adherence to Python standards, and its rapidly growing adoption.

This is what the installation process looks like:

FinanceToolkit % uv sync
Resolved 72 packages in 0.8s
Installed 72 packages in 1.2s
 + six==1.16.0
 + asttokens==2.4.1
 + executing==2.0.0
 + idna==3.4
 + multidict==6.0.4
 + parso==0.8.3
 + platformdirs==3.11.0
 + ...

And this is what adding a new dependency (including extras) looks like:

FinanceToolkit % uv add "pandas[computation,performance,plot]"
Resolved 87 packages in 0.6s
Installed 15 packages in 0.9s
 + pytz==2023.3.post1
 + tzdata==2023.3
 + contourpy==1.2.0
 + cycler==0.12.1
 + fonttools==4.44.0
 + kiwisolver==1.4.5
 + llvmlite==0.41.1
 + pandas==2.1.2
 + pillow==10.1.0
 + pyparsing==3.1.1
 + bottleneck==1.3.7
 + matplotlib==3.8.1
 + numba==0.58.1
 + numexpr==2.8.7
 + xarray==2023.10.1

This addition will then be reflected in the pyproject.toml file:

[project]
requires-python = ">=3.10, <3.13"
dependencies = [
    "pandas[computation,performance,plot]>=2.1.2",  # <-- Added
    "requests>=2.31.0",
    "scikit-learn>=1.3.1",
]

Setting up Linters

Linters are tools that analyze code to detect errors, enforce style guidelines, and suggest improvements. They range from code formatters and spell checkers to tools identifying potential bugs or security vulnerabilities. I recommend the following linters to check code quality and consistency before committing changes via Git:

  • Black: A PEP 8 compliant opinionated formatter, maintained by the Python Software Foundation.
  • Ruff: A fast linter that combines the functionality of tools like Flake8 (and its plugins), isort, pydocstyle, yesqa, eradicate, pyupgrade, and autoflake.
  • ty: A fast type checker by Astral (the creators of Ruff and uv) that verifies type hints to help ensure correct usage of variables and functions.
  • bandit: Designed to find common security vulnerabilities in Python code.
  • codespell: Identifies common misspellings in code and text files.

These linters can be configured within the pyproject.toml file, as shown below:

[tool.ruff]
line-length = 122
select = ["E", "W", "F", "Q", "W", "S", "UP", "I", "PD", "SIM", "PLC", "PLE", "PLR", "PLW"]
ignore = ["S105", "S106", "S107", "PLR0913", "S310", "S301"]
exclude = ["conftest.py"]

[tool.ruff.isort]
combine-as-imports = true
force-wrap-aliases = true

[tool.isort]
profile = "black"
line_length = 122
skip_gitignore = true
combine_as_imports = true

[tool.codespell]
ignore-words-list = 'te,hsi'
skip = '*.json,./.git,pyproject.toml,uv.lock,examples'

[tool.ty]
# See https://docs.astral.sh/ty/ for configuration options

The .pre-commit-config.yaml file configures which linters should be run automatically before code is committed. Running pre-commit install sets up Git hooks based on this file.

I recommend downloading this .pre-commit-config.yaml file as a starting template for your projects. Feel free to modify it, but it provides a solid foundation using common linters.

With the hooks installed, these checks will run automatically on each git commit. For example, the output during a commit might look like this:

(base) FinanceToolkit % git commit -m "v1.5.0 Release featuring Threading"
check for merge conflicts................................................Passed
detect private key.......................................................Passed
black....................................................................Failed
- hook id: black
- files were modified by this hook

reformatted financetoolkit/fundamentals_model.py

All done! ✨ 🍰 ✨
1 file reformatted, 1 file left unchanged.

ruff.....................................................................Failed
- hook id: ruff
- exit code: 1

financetoolkit/helpers.py:75:33: PLR2004 Magic value used in comparison.
Found 1 error.

codespell................................................................Passed
ty.......................................................................Passed

This output shows which linters passed and failed based on the files staged for the commit. In this example, Black failed because it needed to reformat a file, and Ruff failed because it detected a ‘magic value’ (a hardcoded number without clear meaning). There are several ways to address these failures:

  1. Refactor: Introduce a named constant for the magic value. This is generally the preferred approach as it improves code readability.
  2. Suppress: Add a # noqa comment to the specific line causing the issue. This is suitable if the code is already self-explanatory despite the linter warning (e.g., if len(df.columns) == 1: # noqa).
  3. Configure: Add an exception rule in the pyproject.toml or .pre-commit-config.yaml file to ignore this specific error type project-wide. This is generally discouraged as it hides potential issues, but might be acceptable in specific circumstances.

After resolving the issues (e.g., by letting Black reformat and refactoring the magic value, then staging the changes again), the commit can proceed. The benefit of pre-commit hooks is that code quality checks run automatically before the commit is finalized. They apply to every line regardless of who wrote it, which makes them a useful first check on code generated by an AI assistant as well.

(base) FinanceToolkit % git commit -m "v1.5.0 Release featuring Threading"
check for merge conflicts................................................Passed
detect private key.......................................................Passed
black....................................................................Passed
ruff.....................................................................Passed
codespell................................................................Passed
ty.......................................................................Passed
[main eb9140d] v1.5.0 Release featuring Threading
 5 files changed, 683 insertions(+), 729 deletions(-)

Creating a Git Workflow

When working with code, you should follow a Git workflow that separates development from production environments. This matters even more with an assistant: let it work on a separate branch, review its changes as a pull request, and only then merge them into the code you rely on. The complexity of the workflow can depend on whether you are working alone or in a team; avoid unnecessary complexity. For example, requiring self-approval of Pull Requests (PRs) from a feature branch is unnecessary if you are the sole developer.

I recommend using Git even for private projects. Platforms like GitHub offer private repositories for this purpose. The main benefit of Git is version control: you can easily track changes, revert to previous states if mistakes are made, and manage different versions or features concurrently.

A common approach, especially for solo developers, involves at least these branches:

  • main: This branch represents the stable, production-ready code. Direct pushes to main should be avoided; changes should be merged from a development branch.
  • develop: This branch contains the latest development changes, potentially unstable but integrating new features. If working alone, you might push directly here. In a team, changes are typically merged from feature branches.

This leads to a workflow visualized as follows:

gitGraph LR: commit tag:"v1.0.0" branch develop commit commit commit checkout main merge develop tag:"v1.1.0" checkout develop commit commit checkout main merge develop tag:"v1.1.1" checkout develop commit commit commit commit checkout main merge develop tag:"v1.2.0"

When working in a team, you need a more structured workflow with code reviews and testing before merging into the develop branch. This prevents issues in one feature branch from affecting others. Additional branches often used in team workflows include:

  • feature/*: These branches are created for specific new features or tasks. Work is done here, and changes are merged into develop (usually via Pull Requests). Feature branches are typically deleted after merging. The graph below simplifies this by showing a single ‘feature’ branch, but many exist in practice.
  • hotfix/*: These branches are used to address critical bugs found in the production (main) branch that require immediate fixes. They branch off main and are merged back into both main and develop.

This team workflow can be visualized as:

gitGraph LR: commit tag:"v1.0.0" branch feature commit commit branch develop commit checkout feature commit commit checkout develop merge feature checkout main merge develop tag:"v1.1.0" branch hotfix commit checkout main merge hotfix tag:"v1.1.1" checkout feature commit checkout develop merge feature checkout main merge develop tag:"v1.2.0"

This is one common way to structure Git workflows; many variations exist (e.g., Gitflow, GitHub Flow). What matters is that the workflow suits your project and team and clearly separates production code from ongoing development.

Adding Git Exceptions

The .gitignore file specifies intentionally untracked files that Git should ignore. This is useful for excluding generated files (like compiled code), local configuration (IDE settings), sensitive data, or temporary files (like Jupyter Notebook checkpoints). This file is essential when using version control. If you aren’t using Git yet, consider adopting it for its change tracking and versioning.

An example .gitignore file might look like this:

# FinanceToolkit Specific
*.ipynb
!examples/*.ipynb

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.DS_Store
.ipynb_checkpoints/
.DS_Store

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/

Instructions for AI Assistants

If you work with an AI assistant that edits your project directly, such as Claude Code, GitHub Copilot or Cursor, it helps to write down how the project works in a file at the root of the repository. Common names are AGENTS.md, CLAUDE.md or .github/copilot-instructions.md, depending on the tool. The assistant reads this file before it starts, the same way a new colleague would read the README.

Keep it short and specific. For a financial model it could cover:

  • The structure: where models, views, controllers and helpers live, and that every function gets a matching test.
  • The conventions: PEP 8, type hints, Google-style docstrings, and the linters that must pass before a commit.
  • The workflow: which commands install the project (uv sync) and run the tests (pytest tests), and that recorded test output is never rewritten without a review.
  • The domain rules: for example, that ratios are calculated from the reported statements, that percentages are stored as fractions, or which data sources may be used.

Writing this file is also a good test of your own understanding: if you can’t describe your project’s structure in a few lines, it is probably not clear enough yet. Working with AI covers these files in depth, together with rules, Skills, hooks and MCP servers.

Once these setup steps are complete, it’s time to focus on structuring your model’s code. Visit Structure your Model to continue!

Structure your Model