Your Python project structure, also known as your project layout, is the arrangement of files and directories that determines how your code gets imported, tested, and packaged. Choosing between a flat and a src/ layout depends on the type of project you’re building and the trade-offs you accept.
In this tutorial, you’ll turn those choices into confident defaults for libraries and for CLI, web, and GUI applications.
By the end of this tutorial, you’ll understand that:
- The type of project you build, whether a script, an application, or a library, points to the layout that fits.
- A flat layout keeps the importable package at the project root, so you can import it without installing anything.
- A
src/layout hides the package behind an install step, so your tests exercise the same code your users receive. - A thin
__main__.pypaired with a[project.scripts]table inpyproject.tomlis the modern way to define the project’s entry point, and it replaces older approaches likesetup.pyor abin/directory. - Domain-named subpackages like
core/andapi/scale better than catch-all modules likeutils.py.
To follow along, it helps if you’re comfortable with Python modules and packages and if you’ve run a script or two from the command line.
You won’t install anything or build a full application here. Instead, several annotated directory trees will show you why each file belongs where it does.
Get Your Cheat Sheet: Click here to download your free Python project structure cheat sheet and keep the flat vs. src/ decision and the entry-point wiring within reach on your next project.
Take the Quiz: Test your knowledge with our interactive “Python Project Structure: How to Choose the Right Layout” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Project Structure: How to Choose the Right LayoutTest your understanding of Python project structure, from flat and src layouts to organizing packages for CLI, web, and GUI applications.
Understanding Why Python Project Structure Matters
A directory layout can feel like bookkeeping, but it shapes every interaction that you and your teammates have with the code. The payoff of a clear layout shows up in several concrete ways, including the following:
- Lower cognitive load: When every file sits in a descriptively named directory and has a name that signals what it holds, a contributor who opens the repository for the first time can guess where things live instead of grepping around.
- Predictable imports: When your directories mirror the way you import them, you can read a module’s dotted import name straight off the directory tree rather than hunting for where it lives.
- Reliable tests: When your tests find the package and modules just as your users will, running
pytestbehaves the same in continuous integration (CI) as it does on your computer. - Smooth packaging: When you ship a library to PyPI, build a CLI binary, deploy a web service in a container, or freeze a desktop app with PyInstaller, the right layout means the wheel, installer, or image holds exactly what you expect. You end up with no missing assets, no stowaway test files, and no broken entry points.
All of these benefits rest on a vocabulary distinction worth getting straight early. The project is the whole repository, including its configuration, tests, and documentation. The package is the importable code that lives inside the project. Keeping the two ideas separate makes the rest of this tutorial click into place because most layout questions come down to where the package sits relative to everything else.
If you want a refresher on the import machinery behind these ideas, then the project layout reference makes a good companion. With the vocabulary in hand, you can start narrowing your options by naming the type of project that you’re building.
Identifying the Type of Project You’re Building
Before you start learning about layouts, you need to know what you’re laying out. Python projects fall into three broad types, and each one has its own typical layout:
- Scripts: One or a few
.pyfiles that you run directly. They need no installation and often nopyproject.toml, or at most a PEP 723 inline header. A data-cleanup script or acronjob fits here. - Applications: Software that you run or deploy, rather than code that others import. This is where Python application structure varies the most, covering CLIs, web apps, GUI apps, and background workers. Web apps use frameworks like Django, Flask, or FastAPI, while GUI apps rely on tools like PyQt, Tkinter, Toga, or Kivy. You ship them as a container, a wheel, or an installer.
- Libraries: Importable code that other projects depend on. You can distribute a library through PyPI, and your users consume it with
import package_name. Familiar examples include requests, HTTPX, and Rich.
A couple of cases sit at the edges of this taxonomy and could cause confusion:
- A plugin, like
pytest-cov, is a specialized library that gets consumed through a[project.entry-points]table inpyproject.tomlinstead of a directimport. - A hybrid, like
pytest,mypy, orblack, ships as both a library and a command-line tool. The structural rule is to build the project around the more demanding half, usually the library, and then add the CLI as a thincli.pywith a[project.scripts]entry.
Naming the project type is the first step of the layout decision because the type alone points you to a default you can adopt unless you have a reason not to:
| Project Type | Typical Layout | Why |
|---|---|---|
| Scripts | None (flat if it grows) | A script is barely a project, and a flat layout with pyproject.toml is the natural next step. |
| Applications | Flat | Most web frameworks scaffold projects flat, and you switch to src/ when packaging or wheel deployment matters. |
| Libraries | src/ |
Installation is part of the user contract, since you mostly distribute through PyPI or a private index. |
This table shows where you’ll end up. Here’s a short, repeatable workflow that you’ll follow throughout the rest of this tutorial:
- Identify the type of Python project.
- Pick a layout.
- Organize the package from the inside out.
Those three steps will guide your decision-making, starting with the type of project you’re building and the layout it points to:
With the project type settled, take a closer look at the two main layouts that cover nearly every Python project.
Exploring the Flat and src/ Layouts
Two well-established layouts cover the vast majority of Python projects, and the only difference between them is how deep the importable package sits:
- A flat layout puts the package at the project root, right next to
pyproject.toml. - A
src/layout pushes it one level deeper, inside asrc/directory.
A flat layout keeps the importable package and its configuration as close together as possible:
project_name/
│
├── project_name/
│ ├── __init__.py
│ └── ...
│
├── tests/
│
├── pyproject.toml
└── README.md
A src/ layout looks almost identical, except that the package moves down into the src/ directory:
project_name/
│
├── src/
│ └── project_name/
│ ├── __init__.py
│ └── ...
│
├── tests/
│
├── pyproject.toml
└── README.md
The two trees share the same skeleton. The outer project_name/ directory is the project root, which is your repository. The inner project_name/ directory is the main package, which holds your source code. They carry the same name by convention but play different roles, and only the package moves when you switch layouts.
Note: These trees use project_name for both the main package and the project root, which signals that the package is typically named after the project itself. Later sections switch to package_name where the focus is on the package alone.
The project-root files and directories stay the same in both layouts, and each one has a job. Here’s a quick tour of what typically lives there:
pyproject.toml: The build configuration, PEP 621 metadata, and tool settings—gathered under tables like[build-system],[project], and[tool.*]. It replaces the oldersetup.py, and for libraries it can also take over the dependency declarations that used to live inrequirements.txt.README.md: The document that describes what the project is and how to use it.LICENSE: The terms under which others may use your code, needed when you distribute it.- The main package: The importable package itself, named
project_name/at the root in a flat layout andsrc/project_name/in asrc/layout, wheresrc/is a plain wrapper with no Python meaning. tests/: A top-level test directory that mirrors the package.docs/: An optional home for anything longer than theREADME.md.- Configuration and CI: Optional files, such as
.gitignore,.pre-commit-config.yaml,.github/workflows/, anoxfile.pyorMakefile, and.env.example. What configures the project lives at the root, and what is the project lives in the importable package.
To make pyproject.toml concrete, here’s a minimal version that declares how to build the project and what to call it:
pyproject.toml
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "project_name"
version = "1.0.0"
This snippet is enough for a build backend to turn your package into a wheel, the standard prebuilt package format that pip installs.
If you want to go deeper, then the guide on how to manage projects with pyproject.toml walks through the file’s main tables. For now, the key question is where the package sits, so it’s time to weigh the flat layout against the src/ layout.
Using the Flat Layout
A flat layout keeps the package at the root, so you can run your code without installing anything, and a script in the repository can import project_name directly. Most applications use this layout.
For example, the following all sit flat: every project generated by Django’s startproject, the official Flask flaskr/ tutorial package, and FastAPI’s bigger-applications structure.
The convenience comes with one catch. Because the current directory is already on Python’s import path, a flat layout can mask packaging mistakes.
A module that imports fine on your machine might be missing from the wheel entirely, and you wouldn’t notice until a user did. The fix is to run your tests against an installed copy in CI, so the test environment resembles a real install instead of your working directory.
Using the src/ Layout
A src/ layout removes the import issue that a flat layout creates. None of your package’s code becomes importable until you run python -m pip install -e . or a similar editable install, so your tests run against the same installed package that your users will get.
You can watch this difference play out below. Switch between the two layouts and run the steps to see how the same import behaves in each:
Because only the contents of src/ become importable once you install your project, stray Python code at the root, like a tools/ helper or an examples/ directory, can’t sneak onto the import path and shadow your real package.
The official packaging guide spells out these benefits in its discussion of the src/ layout versus the flat layout. There’s a bonus for distribution, too. Since the importable package sits in its own directory, the wheel mirrors src/ one-to-one, so what you see in the tree is what ships.
The trade-off is the editable install itself. Root-level scripts can’t import the package until you install it, and every contributor has to remember that first python -m pip install -e . step.
Choosing Between Flat and src/
The benefits of src/ apply to applications, not just libraries, and the decision is a judgment call rather than a rule. In most cases, you can match your situation to the right layout:
| Your Situation | Flat | src/ |
|---|---|---|
| Writing a script or small CLI | ✅ | |
| Following a framework convention | ✅ | |
| Contributing to an already-flat repo | ✅ | |
| Keeping the setup as simple as possible | ✅ | |
| Publishing a library to PyPI | ✅ | |
| Catching packaging mistakes early in CI | ✅ | |
| Growing past about ten modules | ✅ | |
| Matching the wheel to the repo exactly | ✅ |
As a rule of thumb, a flat layout is a fine starting point until you have a specific reason to switch. Many mature projects never find that reason. When the time does come, migrating is mechanical: you move the package into the src/ directory and point the build backend at it. With setuptools, that’s a single configuration line:
pyproject.toml
[tool.setuptools.packages.find]
where = ["src"]
Once you’ve settled on where the package sits, the next concern is how to organize the code inside it.
Organizing the Source Code
A layout decision doesn’t stop at the package boundary. Inside the importable package, three layers of organization tend to appear in this order as a project grows:
- Modules: The
.pyfiles that hold your code. - An entry point: Normally a thin
__main__.pythat makes the package runnable. - Subpackages: Domain-named directories that group related modules.
That order isn’t arbitrary. You’ll often add an entry-point script once the package is worth running, and subpackages once an undifferentiated pile of modules starts to sprawl. The next three sections take each layer in turn, starting with the modules themselves.
Adding Modules
Modules are the .py files that live inside a package directory. The __init__.py files are also modules, but they play a special role: Their presence marks the containing directory as a regular package that Python can import.
An __init__.py can re-export the names that callers are meant to import, set __all__, and sometimes expose a __version__ constant. For example, the package_name/__init__.py below re-exports two names and sets a version constant:
package_name/__init__.py
from package_name.core import load_config, run
__version__ = "1.0.0"
__all__ = ["load_config", "run"]
In this example, the __init__.py keeps the public API small and intentional, so callers import load_config from the top of the package without reaching into the module where it happens to be defined.
A few best practices will keep this file healthy:
- Avoid doing expensive work at import time.
- Steer clear of side effects like network calls or file writes.
- Watch for circular imports, since the main package’s
__init__.pyruns whenever anyone imports the package.
All three guidelines keep the main package’s __init__.py lightweight, so importing the package stays fast and free of surprises. With your modules in order, the next layer turns the package into something you can run.
Adding Entry Points
When you want to run your project from the command line, you need an entry-point script. In a modern project, that script is a __main__.py file rather than a bin/ directory or another legacy approach. Keeping __main__.py thin is also a best practice. Typically, it imports a main() function from a module like cli.py or app.py and calls it. The example below uses cli.py:
package_name/__main__.py
from package_name.cli import main
main()
As the Python documentation for __main__ explains, this file runs whenever someone executes python -m package_name. The docs recommend leaving out the if __name__ == "__main__" idiom here, since __main__.py files stay short and skipping the guard keeps the behavior consistent across regular packages and zip archives.
To turn that entry point into a real command, you add a [project.scripts] table to pyproject.toml:
pyproject.toml
[project.scripts]
package_name = "package_name.cli:main"
On install, that line generates a package_name executable on the user’s PATH, wired straight to your main() function. Your users get a standalone command without ever typing python -m.
Grouping in Subpackages
As modules multiply, you’ll feel the pull to dump loose functions into catch-all modules named utils.py or helpers.py. That practice causes trouble in a few ways. These names say nothing about what’s inside, so contributors can’t predict where to add a new function and end up piling unrelated code together.
These modules accumulate imports from across the codebase and commonly turn into circular-import hotspots. Worst of all, once everyone depends on them, even small changes risk breaking distant callers.
Domain-named subpackages scale better than a junk drawer. Each one is a package in its own right. Subpackages like package_name/core/, package_name/api/, and package_name/cli/ each carry a clear responsibility. They can expose a narrow public surface through __init__.py and keep their dependencies flowing in one direction.
Inside them, use absolute and relative imports deliberately, defaulting to absolute ones for clarity. With these three layers in mind, you can see how real projects put them to work.
Applying Layouts in Practice: Libraries, CLI, Web, and GUI
The patterns you’ve seen so far come together in complete Python project structures. The examples below walk from a library to a CLI, through the major web frameworks, and on to a GUI app. The library uses a src/ layout, and the applications that follow all stay flat, which mirrors the typical layouts from earlier in this tutorial.
A Library: Small Statistics Helper
Say that you’re writing a small statistics helper called statkit. Because a library exists only to be imported and never run, it shows the layout with the fewest moving parts. It’s also a natural candidate for publishing to PyPI, which is why it uses a src/ layout.
Here’s the tree for a hypothetical statkit library that ships statistics functions in separate modules:
statkit/
│
├── src/
│ └── statkit/
│ ├── __init__.py ← API: mean, median, stdev, pearson, spearman
│ ├── correlation.py ← Define: pearson, spearman
│ ├── descriptive.py ← Define: mean, median, stdev
│ └── py.typed ← PEP 561 marker, ships type hints
│
├── tests/
│ ├── test_correlation.py ← Tests for correlation.py
│ └── test_descriptive.py ← Tests for descriptive.py
│
├── docs/
│
├── LICENSE
├── pyproject.toml
└── README.md
Beyond the [build-system] and [project] boilerplate, the src/ layout needs only the single table you saw earlier, which points the build backend at the package under src/:
pyproject.toml
[tool.setuptools.packages.find]
where = ["src"]
Because pure libraries aren’t runnable, you have no __main__.py and no [project.scripts] table. The public API surface lives in __init__.py, which re-exports functions like mean(), median(), stdev(), pearson(), and spearman(). Listing them in the __all__ attribute keeps callers from reaching into descriptive.py or correlation.py directly.
Here’s what that looks like:
src/statkit/__init__.py
from statkit.descriptive import mean, median, stdev
from statkit.correlation import pearson, spearman
__all__ = ["mean", "median", "stdev", "pearson", "spearman"]
Here, two files matter more than in the application examples. A LICENSE is effectively required. PyPI accepts uploads without one, but no downstream project can legally depend on code that has no license.
An empty py.typed file inside the package, defined by PEP 561, tells type checkers like mypy and Pyright that the library ships its own type hints. Without it, type checkers silently ignore your annotations and treat every export as Any.
When you’re ready to share your library, the guide on how to publish a package to PyPI picks up where this layout leaves off.
A CLI Application: Weather Report Tool
A weather report tool is a good example of a small CLI, and it shows the entry-point conventions in a flat application layout:
weatherly/
│
├── weatherly/
│ │
│ ├── sources/
│ │ ├── __init__.py
│ │ └── ... ← API clients, one per provider
│ │
│ ├── __init__.py
│ ├── __main__.py ← Enables python -m weatherly
│ ├── cli.py ← Argument parsing and main()
│ └── forecast.py ← Fetch and format the forecast
│
├── tests/
│
├── pyproject.toml
└── README.md
The weatherly command comes from a single table in pyproject.toml:
pyproject.toml
[project.scripts]
weatherly = "weatherly.cli:main"
In this example, the entry-point string "weatherly.cli:main" is an import path, not a path on disk, so moving the package into src/weatherly/ for a src/ layout wouldn’t change it. You can make this move if the project grows and you want the tests to run against an installed copy.
Note: To build your own CLI weather app, check out the Raining Outside? Build a Weather CLI App With Python tutorial.
The one practical consequence of migrating to src/ is that python -m weatherly from the project root stops working until an editable install like python -m pip install -e . puts the package back on the import path.
A Django Project: Personal Portfolio Site
A personal portfolio site is a comfortable size for a Django project. Here, Django’s conventions assume the flat layout, and the framework’s own tooling generates most of this tree:
portfolio/
│
├── apps/
│ ├── blog/
│ ├── contact/
│ └── projects/ ← Portfolio entries
│
├── portfolio/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
│
├── static/
│
├── templates/
│
├── tests/
│
├── manage.py
├── pyproject.toml
└── README.md
Running django-admin startproject puts manage.py at the root, along with the inner project package and its settings.py, urls.py, asgi.py, and wsgi.py. Then python manage.py startapp creates each app next to manage.py by default.
Note: In the case of projects built with established frameworks, such as Django, Flask, and FastAPI, your flexibility in choosing the layout is limited. The framework often decides how to scaffold the project for you, and if you don’t follow that structure, then the project may not work correctly.
Grouping those apps under an apps/ directory is a convention that many larger projects adopt to keep the root tidy once they outgrow two or three apps. It isn’t a free move, though: Because it changes each app’s import path, you need to update the app’s AppConfig.name and its INSTALLED_APPS entry so Django can still find it.
Django apps are Python packages in their own right, so the same __init__.py rules apply. A src/ layout is unusual for Django projects, but possible.
Note: To build your own portfolio site with Django, check out the Get Started With Django: Build a Portfolio App tutorial.
For everything past the layout, the Django tutorials cover the framework itself, and the walkthrough on how to set up a Django project starts you from scratch.
A Flask Project: Personal Blog
A personal blog suits Flask, where the package’s __init__.py does real work instead of merely re-exporting names. As with Django, Flask projects follow the framework’s flat convention.
Here’s the directory tree for a flat Flask layout:
bloggy/
│
├── bloggy/
│ │
│ ├── static/
│ │
│ ├── templates/
│ │ ├── auth/
│ │ ├── posts/
│ │ └── base.html
│ │
│ ├── __init__.py ← Application factory
│ ├── auth.py ← Auth blueprint
│ ├── db.py
│ └── posts.py ← Posts blueprint
│
├── tests/
│
├── pyproject.toml
└── README.md
Here, the __init__.py file is an application factory that creates the Flask app and registers blueprints. Each blueprint module, like auth.py and posts.py, owns a slice of the URL space and a matching subdirectory under templates/.
Note: To build your own database-backed Flask web app, check out the Enhance Your Flask Web Project With a Database tutorial.
The official Flask tutorial uses this same flat layout, with blog.py in place of posts.py. Real projects often keep non-Python data files like schema.sql next to db.py.
To go beyond the layout, check out the guide on how to structure a scalable Flask project and the broader Flask tutorials.
A FastAPI Project: Book-Catalog API
A book-catalog API is a natural fit for FastAPI, where an APIRouter provides the same modularity that Django apps and Flask blueprints do.
Here’s the directory tree for a FastAPI project:
bookapi/
│
├── bookapi/
│ │
│ ├── internal/
│ │ ├── __init__.py
│ │ └── admin.py ← Admin-only endpoints
│ │
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── authors.py ← Author endpoints
│ │ └── books.py ← Book endpoints
│ │
│ ├── __init__.py
│ ├── dependencies.py ← Auth and DB session helpers
│ └── main.py ← FastAPI app, includes routers
│
├── tests/
│
├── pyproject.toml
└── README.md
The FastAPI bigger-applications tutorial uses exactly this shape, where main.py builds the app and pulls in each router.
Unlike the CLI example and the GUI app you’ll see next, this project has no __main__.py. You launch a FastAPI app by pointing an ASGI server like Uvicorn at bookapi.main:app, not by running the package as a module.
Every subpackage, like routers/ and internal/, gets its own __init__.py so that the relative imports inside each module resolve correctly.
Note: To build your own API with FastAPI, check out the Build a URL Shortener With FastAPI and Python tutorial.
A flat layout works well for a microservice deployed straight from a repository, while src/bookapi/ makes sense once you start shipping a wheel or a container. If FastAPI is new to you, then the tutorial on how to get started with FastAPI covers the framework behind this layout.
A GUI Application: Desktop Notes App
A desktop notes app rounds out the examples, and GUI apps add a wrinkle that the others don’t: They usually split the package along an architectural pattern. The common choices are MVC (Model-View-Controller), MVP (Model-View-Presenter), and MVVM (Model-View-ViewModel). All of them keep input handling, state, and rendering testable in isolation.
The example below is a PyQt or PySide app that uses MVC, but the same shape works for MVP or MVVM if you rename controllers/ to presenters/ or viewmodels/. Here’s the layout sketch:
notely/
│
├── notely/
│ │
│ ├── controllers/
│ │ ├── __init__.py
│ │ └── ... ← Input handlers, app coordination
│ │
│ ├── models/
│ │ ├── __init__.py
│ │ └── ... ← Note data and persistence
│ │
│ ├── resources/
│ │ ├── icons/
│ │ ├── translations/
│ │ └── __init__.py
│ │
│ ├── views/
│ │ ├── ui/ ← Qt Designer .ui files
│ │ ├── __init__.py
│ │ └── ... ← Screens, dialogs, main window
│ │
│ ├── __init__.py
│ ├── __main__.py
│ └── app.py ← Wires models, views, controllers
│
├── tests/
│
├── pyproject.toml
└── README.md
Two more GUI-specific twists go beyond the CLI conventions. The first is assets. Keep content like icons and translations in resources/, and keep Qt .ui files with the views they define under views/ui/.
In both cases, those directories live inside the importable package so that they travel with the code when you distribute the app. Load them through importlib.resources.files() rather than relative paths built from __file__, which are unreliable inside zip imports and frozen bundles like PyInstaller, py2app, and cx_Freeze.
In pyproject.toml, two tables back these choices. One table gives the app a launch command, and the other declares resources/ and the .ui files as package data so the build includes them:
pyproject.toml
[project.scripts]
notely = "notely.app:main"
[tool.setuptools.package-data]
notely = ["resources/**/*", "views/ui/*.ui"]
The second twist is distribution. Desktop apps that aren’t scaffolded by a tool follow no single convention. Spyder keeps a flat layout, while Calibre uses a src/ layout, and each has its own build pipeline: Calibre freezes the interpreter, and Spyder builds conda-based installers. Briefcase, by contrast, bootstraps projects as src/ layouts from the start.
Note: To build your own data-backed desktop app, check out the Build a Contact Book With Python, PyQt, and SQLite tutorial.
The layout choice depends as much on your packaging tool as on your GUI framework. To dig into PyQt itself, check out the GUI Programming With PyQt learning path. With these concrete examples on the table, a side-by-side comparison brings the pattern into focus.
Comparing the Layouts at a Glance
Lined up by project type, the examples reveal both shared patterns and genuine differences:
| Project Type | Layout | Entry Point | Code Split By | Typical Distribution |
|---|---|---|---|---|
| Library | src/ |
None, import only | Modules | PyPI wheel |
| CLI application | Flat | __main__.py and [project.scripts] |
Modules and a subpackage | CLI binary on install |
| Django project | Flat | manage.py plus a WSGI app |
Django apps | Container or host |
| Flask project | Flat | Application factory | Blueprints | Container or host |
| FastAPI project | Flat | ASGI app in main.py |
APIRouter subpackages |
Container or host |
| GUI application | Flat | __main__.py and [project.scripts] |
MVC layers | Frozen executable |
Read down the columns, and the pattern is clear. Every application project uses the flat layout, while the library reaches for src/. Every runnable project funnels execution through a thin, well-defined entry point.
What changes from one project to the next is how each one splits its code—whether into Django apps, Flask blueprints, FastAPI routers, or GUI layers—and how it reaches its users. The skeleton stays the same, and your framework determines what you hang on it.
Conclusion
You now have a repeatable way to approach Python project structure, so a new repository takes shape from a few deliberate decisions instead of inheriting the shape of your last project. The big choices come down to a short sequence: name the type of project, pick a layout, and organize the package from the inside out.
Get that sequence right, and the payoffs follow, from predictable imports to reliable tests and smooth packaging.
In this tutorial, you’ve learned how to:
- Tell scripts, applications, and libraries apart, and recognize each one’s typical layout
- Weigh the flat layout against the
src/layout using their trade-offs - Organize a package with focused modules, a thin entry point, and domain-named subpackages
- Apply those patterns to library, CLI, web, and GUI projects
Reach for a flat layout when you want to move fast, and switch to src/ when packaging correctness starts to matter. Either way, you can start from a blank project directory and place your source code, tests, docs, scripts, and pyproject.toml with confidence. To keep your environment as tidy as your tree, read the primer on Python virtual environments next.
Get Your Cheat Sheet: Click here to download your free Python project structure cheat sheet and keep the flat vs. src/ decision and the entry-point wiring within reach on your next project.
Frequently Asked Questions
Now that you’ve seen how to structure a Python project, you can use the questions and answers below to check your understanding and recap what you’ve learned.
These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.
There’s no single best structure, because the right one depends on whether you’re building a script, an application, or a library. Most projects start with a flat layout and move the package into a src/ directory only when packaging correctness starts to matter.
A flat layout keeps the importable package at the project root, so you can import it without installing anything. A src/ layout moves the package one level down into a src/ directory, so nothing imports until you install the project, which makes your tests run against the same package your users receive.
Put a top-level tests/ directory at the project root, next to pyproject.toml, so the directory mirrors your package without shipping inside it. Keeping tests outside the importable package leaves them out of your wheel, but your tooling can still find them.
A quick script can run without one, but the moment you want to install the project, declare dependencies, or build a wheel, pyproject.toml becomes the standard home for that configuration. The guide on how to manage projects with pyproject.toml covers what goes inside it.
A __main__.py file makes a package runnable, so python -m package_name executes it. Keeping it thin, with a single call to a main() function defined elsewhere, is the modern replacement for an old-style bin/ script.
Take the Quiz: Test your knowledge with our interactive “Python Project Structure: How to Choose the Right Layout” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Project Structure: How to Choose the Right LayoutTest your understanding of Python project structure, from flat and src layouts to organizing packages for CLI, web, and GUI applications.