How to Create a Python Package

What you’ll build or solve

You’ll turn a folder of Python files into an importable package with a clean structure and one reliable entry point.

When this approach works best

This approach works best when you:

  • Split code into multiple files and want imports that keep working as the project grows.
  • Plan to reuse your code across scripts, projects, or teammates.
  • Want a clean “public API” so others import from one place, not random files.

Avoid this approach when:

  • You have a single small script and no plans to reuse it. A package structure will slow you down.

Prerequisites

  • Python 3 installed
  • You know what a file and folder are
  • You know basic imports like import os and from x import y
  • You can run terminal commands

Step-by-step instructions

1) Create a project folder with a src layout

A src/ layout prevents accidental imports from your working directory during development. Without it, Python can sometimes import your package “by coincidence” because the current folder is on sys.path, and that can hide packaging problems until later.

Project structure:

my_project/
  src/
    my_package/
      __init__.py
      utils.py
  pyproject.toml

Create the folders and files:

mkdir-p my_project/src/my_package
cd my_project
touch src/my_package/__init__.py src/my_package/utils.py pyproject.toml

2) Add some code inside the package

Put reusable code inside your package folder.

src/my_package/utils.py:

defslugify(text:str) ->str:
text=text.strip().lower()
returntext.replace(" ","-")

3) Decide what your package exports in __init__.py

Use __init__.py to define what people should import from your package.

src/my_package/__init__.py:

from.utilsimportslugify

__all__= ["slugify"]

What to look for:

Re-exporting names in __init__.py lets users write from my_package import slugify instead of reaching into internal modules.


4) Add a minimal pyproject.toml

This file tells packaging tools how to build and install your project.

  • setuptools is the build tool that turns your package into an installable project.
  • name is the install name shown in pip. It often uses hyphens.
  • Your folder on disk usually uses underscores because Python imports use underscores.
  • dependencies lists packages your code needs at runtime (like requests).

pyproject.toml:

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
version = "0.1.0"
description = "Example package"
requires-python = ">=3.9"

dependencies = [
  "requests>=2.28.0",
]

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

What to look for:

  • Use hyphens in [project].name (pip install my-package), and underscores in your folder name (src/my_package) and imports (import my_package).
  • Change requires-python if your code needs newer Python features.
  • Keep dependencies aligned with what you import in your package.

5) Install your package in editable mode

Editable install lets you import the package while you keep editing the code.

python-m pip install-e .

What to look for:

Running pip as python -m pip helps you install into the same Python that runs your scripts.


6) Test imports from a separate script

Create a script outside the package folder.

test_import.py (in my_project/):

frommy_packageimportslugify

print(slugify("Hello Package"))

Run it from the project root:

python test_import.py

7) Make the package runnable with python -m

This is useful when your package has a “main” action, like a small CLI or a quick demo. Python looks for my_package/__main__.py when you run python -m my_package.

Add:

src/my_package/__main__.py

src/my_package/__main__.py:

frommy_packageimportslugify

defmain() ->None:
print(slugify("Run with -m"))

if__name__=="__main__":
main()

Run it:

python-m my_package

What to look for:

Running with -m uses the package context, so imports behave more like they will in real usage than when you run internal files directly.


Examples you can copy

1) Small utility package

my_project/
  src/
    text_tools/
      __init__.py
      clean.py
  pyproject.toml

src/text_tools/clean.py:

defnormalize_spaces(s:str) ->str:
return" ".join(s.split())

src/text_tools/__init__.py:

from.cleanimportnormalize_spaces

__all__= ["normalize_spaces"]

2) When to keep it flat vs nest subpackages

Keep it flat when you have a few modules and one clear theme:

src/my_package/
  __init__.py
  utils.py
  validators.py
  formatters.py

Nest into subpackages when you have distinct areas that would become clutter in one folder, or when modules naturally group together:

src/my_app/
  __init__.py
  api/
    __init__.py
    client.py
  core/
    __init__.py
    config.py

A simple rule: if you have more than ~6–8 modules in one folder, or you often say “this part is API code” vs “this part is core logic,” nesting usually helps.


3) Public API pattern for cleaner imports

Goal: allow from my_package import slugify instead of from my_package.utils import slugify.

src/my_package/__init__.py:

from.utilsimportslugify

__all__= ["slugify"]

User code:

frommy_packageimportslugify

print(slugify("Clean Imports"))

Common mistakes and how to fix them

Mistake 1: Running a module file directly and breaking imports

You might run:

python src/my_package/utils.py

Why it breaks:

Running a file inside a package as a script can change how Python resolves imports, so relative imports and package context can break.

Correct approach:

Run from the project root using -m or a separate script:

python-m my_package

Or:

python test_import.py

Mistake 2: Mixing up the install name and the import name

You might set name = "my_package" in pyproject.toml, then try:

importmy-package

Why it breaks:

The install name (often hyphenated) is for pip, while the import name must be a valid Python identifier (underscores, no hyphens).

Correct approach:

  • Use hyphens in pyproject.toml:
name = "my-package"
  • Use underscores in folders and imports:
src/my_package/
importmy_package

Mistake 3: Installing but still seeing ModuleNotFoundError

You might run:

pip install-e .
python test_import.py

Why it breaks:

pip might install into a different Python than the one running your script.

Correct approach:

python-m pip install-e .
python test_import.py

Troubleshooting

If you see ModuleNotFoundError: No module named 'my_package', confirm you ran python -m pip install -e . from the project root, then run your script again from that same root.

If you see ImportError: attempted relative import with no known parent package, avoid running files inside the package directly. Use python -m my_package or run a top-level script.

If you see No module named pip, run:

python-m ensurepip--upgrade

If your editor can’t find imports but the code runs, select the correct interpreter in your editor, then restart the language server.


Quick recap

  • Use a src/ layout to avoid accidental imports during development.
  • Put code under src/<package_name>/.
  • Use underscores for import/package folders, hyphens for the install name in pyproject.toml.
  • Add dependencies in pyproject.toml when your code imports other packages.
  • Install in editable mode with python -m pip install -e ..
  • Run with python -m my_package or a script from the project root.