38.1 Python Project Structure
A Python project structure is the arrangement of folders and files inside a project. A clean structure helps programmers understand where source code, tests, documentation, configuration, and package information belong. Beginners should organize projects from the beginning because a well-arranged project is easier to test, improve, share, and publish.
Example: Basic Python package structure
my_package_project/
│
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── calculator.py
│
└── tests/
└── test_calculator.py
Output:
A project containing package code, configuration, documentation, licensing, and tests.
Explanation: The source code is stored inside the src folder. Tests are stored separately, while project information is kept in files such as pyproject.toml, README.md, and LICENSE.
38.2 Source Layout
The source layout places the actual Python package inside a folder named src. This prevents Python from accidentally importing files directly from the project folder during development. It helps developers test the installed package instead of an unintended local copy, making package testing more accurate and professional.
Example: Package inside the src folder
weather_tools/
│
├── pyproject.toml
└── src/
└── weather_tools/
├── __init__.py
└── converter.py
# converter.py
# Define a function that converts Celsius to Fahrenheit.
def celsius_to_fahrenheit(celsius):
# Apply the temperature conversion formula.
return (celsius * 9 / 5) + 32
Output:
The weather_tools package is stored inside the src folder.
Explanation: The outer folder is the complete project. The inner weather_tools folder is the importable Python package that contains the actual program code.
38.3 pyproject.toml
The pyproject.toml file is the main configuration file for many modern Python projects. It tells packaging tools how the project should be built and provides important project information. It can contain the package name, version, author, Python requirement, dependencies, scripts, and build-system settings.
Example: Basic pyproject.toml file
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "beginner-calculator"
version = "1.0.0"
description = "A simple calculator package for beginners"
requires-python = ">=3.10"
Output:
The project can be recognized and built by modern Python packaging tools.
Explanation: The build-system section selects the tool used to create the package. The project section describes the package and states which Python versions can run it.
38.4 Package Metadata
Package metadata is information that describes a Python package. It helps users understand what the package does, who created it, which Python versions it supports, and where its documentation or source code can be found. Package indexes such as PyPI display this information on the package page.
Example: Adding package metadata
[project]
name = "text-helper-tools"
version = "1.0.0"
description = "Beginner tools for working with text"
readme = "README.md"
authors = [
{name = "Example Developer", email = "developer@example.com"}
]
keywords = ["text", "beginner", "utilities"]
requires-python = ">=3.10"
[project.urls]
Homepage = "https://example.com"
Documentation = "https://example.com/docs"
Repository = "https://example.com/repository"
Output:
Package name: text-helper-tools
Version: 1.0.0
Description: Beginner tools for working with text
Explanation: Packaging tools read this information and include it in the published package. Users can then see the author, description, keywords, and useful project links.
38.5 Package Versions
A package version identifies a particular release of a project. Versions help users understand whether a release contains a major change, a new feature, or a small correction. A common version pattern uses three numbers: major, minor, and patch, such as 2.3.1.
Example: Store and display a package version
# __init__.py
# Store the current package version.
__version__ = "1.2.0"
# Import the package.
import my_package
# Print the installed package version.
print(my_package.__version__)
Explanation: Version 1.2.0 usually means major version 1, minor version 2, and patch version 0. Developers should increase the version before publishing a new release.
38.6 Dependencies
A dependency is another package that your project needs in order to work. When dependencies are listed in pyproject.toml, package installation tools can install them automatically. Developers should include only packages that are truly required and should use reasonable version requirements to reduce compatibility problems.
Example: Declare required dependencies
[project]
name = "website-status-checker"
version = "1.0.0"
dependencies = [
"requests>=2.31.0",
"rich>=13.0.0"
]
# Import a required dependency.
import requests
# Send a request to a website.
response = requests.get("https://example.com", timeout=10)
# Print the response status code.
print(response.status_code)
Explanation: The requests package is required by the project. A status code of 200 normally means the request was successful.
38.7 Optional Dependencies
Optional dependencies provide extra features that are not required for the main package to work. For example, users may install additional packages for testing, documentation, database support, or advanced output. Grouping optional dependencies keeps the basic package smaller while allowing users to install extra capabilities when needed.
Example: Add development and documentation extras
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.4"
]
docs = [
"sphinx>=7.0"
]
# Install the package with development tools.
pip install -e ".[dev]"
# Install the package with documentation tools.
pip install -e ".[docs]"
Output:
The selected optional dependency group is installed.
Explanation: The package can be installed normally without these tools. Developers can choose the dev group, while documentation writers can choose the docs group.
38.8 Entry Points
An entry point connects an installed command or plugin name to a Python function. It allows users to start a program without manually locating and running its Python file. Entry points are often used for command-line applications because they create a simple command during package installation.
Example: Define a package entry point
[project.scripts]
greet-user = "greeting_tools.cli:main"
# cli.py
# Define the function connected to the command.
def main():
# Print a message when the command runs.
print("Welcome to Greeting Tools!")
Output:
Welcome to Greeting Tools!
Explanation: After installation, the greet-user command runs the main function located inside the package’s cli.py module.
38.9 Command-Line Scripts
A command-line script is a program that users run from a terminal. Python packages can provide commands for calculations, file processing, automation, reporting, and many other tasks. A command-line script usually reads user arguments, performs an operation, prints a result, and handles incorrect input clearly.
Example: Simple command-line greeting script
# cli.py
# Import sys to read command-line arguments.
import sys
# Define the main program function.
def main():
# Check whether the user supplied a name.
if len(sys.argv) < 2:
print("Please provide your name.")
return
# Read the first supplied argument.
name = sys.argv[1]
# Print a personalized greeting.
print(f"Hello, {name}!")
# Run main only when this file is started directly.
if __name__ == "__main__":
main()
Command:
python cli.py Sara
Explanation: The name Sara becomes a command-line argument. The program reads it from sys.argv and includes it in the printed greeting.
38.10 Building Packages
Building a package means converting the project files into distribution files that other people can install. Modern Python projects commonly use the build package. Before building, developers should confirm that the package structure, metadata, version, dependencies, README, and license are correct.
Example: Build a Python package
# Install the package-building tool.
python -m pip install --upgrade build
# Build the project from the folder containing pyproject.toml.
python -m build
Output:
dist/
├── beginner_calculator-1.0.0.tar.gz
└── beginner_calculator-1.0.0-py3-none-any.whl
Explanation: The build command usually creates a source distribution and a wheel inside the dist folder. These files can later be tested or uploaded.
38.11 Source Distributions
A source distribution contains the project’s source files and packaging information in a compressed archive. Its filename usually ends with .tar.gz. When a user installs it, the package may need to be built on the user’s computer before installation can finish.
Example: Source distribution file
dist/
└── text_helper_tools-1.0.0.tar.gz
# Install the source distribution locally.
python -m pip install dist/text_helper_tools-1.0.0.tar.gz
Output:
Successfully installed text-helper-tools-1.0.0
Explanation: The archive contains the project source. The installation tool extracts it, builds the package when necessary, and installs it into the active Python environment.
38.12 Wheels
A wheel is a built Python distribution whose filename ends with .whl. Wheels are normally faster to install than source distributions because much of the preparation has already been completed. A pure-Python wheel may work on many operating systems and Python environments without platform-specific compilation.
Example: Install a wheel file
dist/
└── text_helper_tools-1.0.0-py3-none-any.whl
# Install the wheel file.
python -m pip install dist/text_helper_tools-1.0.0-py3-none-any.whl
Output:
Successfully installed text-helper-tools-1.0.0
Explanation: The words py3-none-any usually indicate that the wheel supports Python 3, does not require a specific Python application binary interface, and is not limited to one operating system.
38.13 PyPI Introduction
PyPI is the Python Package Index. It is the main public service used to distribute Python packages. When developers publish a package on PyPI, users can usually install it with pip. Package names on PyPI must be unique, so developers should search before choosing a final name.
Example: Install a package from PyPI
# Install the requests package from PyPI.
python -m pip install requests
# Import the installed package.
import requests
# Print the installed version.
print(requests.__version__)
Output:
A version number such as:
2.32.3
Explanation: The installation command downloads the package and suitable dependencies from PyPI. The exact displayed version may differ depending on the version installed in the environment.
38.14 TestPyPI
TestPyPI is a separate testing service that works similarly to PyPI. It lets developers practise uploading and installing packages without publishing them to the main package index. It is useful for checking metadata, package files, README formatting, and the general publishing process before a real release.
Example: Upload and install through TestPyPI
# Install Twine, which uploads distribution files.
python -m pip install --upgrade twine
# Upload the built files to TestPyPI.
python -m twine upload --repository testpypi dist/*
# Install the test package from TestPyPI.
python -m pip install --index-url https://test.pypi.org/simple/ example-package-name
Output:
The package is uploaded to TestPyPI and can be installed for testing.
Explanation: TestPyPI uses a different package index from the main PyPI service. A TestPyPI account and an appropriate authentication method are required before uploading.
38.15 Publishing Packages
Publishing makes a built package available from a package index. Before publishing, developers should run tests, check the version, review metadata, build fresh distribution files, and inspect them. Authentication information must be protected carefully and should never be written directly inside public source files.
Example: Check and upload package files
# Check package metadata and README formatting.
python -m twine check dist/*
# Upload the distributions to the main PyPI service.
python -m twine upload dist/*
Output:
Checking distribution files: PASSED
Uploading distributions...
Package uploaded successfully.
Explanation: The check command finds common packaging problems before uploading. Twine then sends the source distribution and wheel to the selected package index.
38.16 Updating Packages
Updating a published package requires creating a new release with a new version number. A previously published version normally cannot simply be replaced with different files. Developers should change the code, update tests and documentation, increase the version, rebuild the project, and upload the new distribution files.
Example: Update from version 1.0.0 to 1.0.1
[project]
name = "beginner-calculator"
version = "1.0.1"
# Remove old build folders before rebuilding.
rm -rf build dist
# Build the new package release.
python -m build
# Upload the new release.
python -m twine upload dist/*
Output:
beginner-calculator version 1.0.1 is published.
Explanation: Version 1.0.1 can represent a small correction to version 1.0.0. Users can upgrade by installing the newer release.
38.17 Package Documentation
Package documentation teaches users how to install, import, configure, and use a package. A useful package should normally include a README, installation instructions, examples, an API description, supported Python versions, and contribution information. Clear documentation can be as important as the code itself.
Example: Beginner README content
# Beginner Calculator
Beginner Calculator is a small Python package for basic arithmetic.
## Installation
python -m pip install beginner-calculator
## Example
from beginner_calculator import add
result = add(4, 6)
print(result)
## Output
10
Output:
Users can understand how to install and use the package.
Explanation: The README introduces the project and provides a complete example. Commands, Python code, and expected output help users understand how the package works.
38.18 Package Licensing
A software license explains what other people are allowed to do with a package. It may permit users to use, copy, modify, or redistribute the code under certain conditions. Developers should choose a license carefully and include its complete text in a file commonly named LICENSE.
Example: Declare license information
my_project/
│
├── LICENSE
├── README.md
├── pyproject.toml
└── src/
└── my_package/
└── __init__.py
[project]
name = "my-package"
version = "1.0.0"
license = {file = "LICENSE"}
Output:
The package includes a license file and identifies it in the project metadata.
Explanation: The license file contains the full legal terms. The metadata points packaging tools and users to that file. Developers should not invent a license without understanding its meaning.
38.19 Open-Source Package Development
Open-source development allows other people to view the code and, depending on the license and project rules, report problems or suggest improvements. A healthy open-source package usually has clear documentation, contribution instructions, tests, issue templates, release notes, code-review rules, and respectful communication between contributors.
Example: Contribution workflow
# Download the repository.
git clone https://example.com/example/my-package.git
# Enter the project folder.
cd my-package
# Create a new branch for a change.
git checkout -b fix-add-function
# Run the project tests.
python -m pytest
# Save the completed change.
git add .
git commit -m "Fix add function validation"
Output:
A separate branch contains the tested contribution.
Explanation: Contributors should normally make changes on a separate branch, run the tests, write a clear commit message, and submit the change for review instead of changing the main branch directly.
38.20 Chapter Package Project
In this chapter project, you will create a package named beginner_math_tools. It will include reusable arithmetic functions, package metadata, a README, a license reference, tests, and a command-line command. This project combines the most important ideas from the chapter into one complete beginner-friendly package.
Step 1: Create the project structure
beginner-math-tools/
│
├── pyproject.toml
├── README.md
├── LICENSE
│
├── src/
│ └── beginner_math_tools/
│ ├── __init__.py
│ ├── operations.py
│ └── cli.py
│
└── tests/
└── test_operations.py
Step 2: Create operations.py
# operations.py
# Define a function that adds two numbers.
def add(first_number, second_number):
# Return the addition result.
return first_number + second_number
# Define a function that subtracts the second number.
def subtract(first_number, second_number):
# Return the subtraction result.
return first_number - second_number
# Define a function that multiplies two numbers.
def multiply(first_number, second_number):
# Return the multiplication result.
return first_number * second_number
# Define a safe division function.
def divide(first_number, second_number):
# Prevent division by zero.
if second_number == 0:
raise ValueError("The second number cannot be zero.")
# Return the division result.
return first_number / second_number
Step 3: Export package functions
# __init__.py
# Import the public functions from operations.py.
from .operations import add, subtract, multiply, divide
# Store the package version.
__version__ = "1.0.0"
# List the names that are intended for public use.
__all__ = [
"add",
"subtract",
"multiply",
"divide"
]
Step 4: Create the command-line program
# cli.py
# Import the package functions.
from .operations import add, subtract, multiply, divide
# Define the command-line program.
def main():
# Display the available operations.
print("Beginner Math Tools")
print("1. Add")
print("2. Subtract")
print("3. Multiply")
print("4. Divide")
# Ask the user to choose an operation.
choice = input("Choose an operation: ")
# Ask for two numbers and convert them to floats.
first_number = float(input("Enter the first number: "))
second_number = float(input("Enter the second number: "))
# Select the correct function.
if choice == "1":
result = add(first_number, second_number)
elif choice == "2":
result = subtract(first_number, second_number)
elif choice == "3":
result = multiply(first_number, second_number)
elif choice == "4":
result = divide(first_number, second_number)
else:
print("Invalid operation.")
return
# Display the final answer.
print(f"Result: {result}")
# Run the program when this file starts directly.
if __name__ == "__main__":
main()
Step 5: Create pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "beginner-math-tools"
version = "1.0.0"
description = "Simple reusable math tools for Python beginners"
readme = "README.md"
requires-python = ">=3.10"
authors = [
{name = "Example Developer"}
]
license = {file = "LICENSE"}
keywords = ["math", "calculator", "beginner", "education"]
[project.optional-dependencies]
dev = [
"pytest>=8.0"
]
[project.scripts]
beginner-math = "beginner_math_tools.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
Step 6: Create a test
# test_operations.py
# Import the package functions.
from beginner_math_tools import add, subtract, multiply, divide
# Test the add function.
def test_add():
assert add(4, 6) == 10
# Test the subtract function.
def test_subtract():
assert subtract(10, 3) == 7
# Test the multiply function.
def test_multiply():
assert multiply(5, 4) == 20
# Test the divide function.
def test_divide():
assert divide(12, 3) == 4
Step 7: Install and test the project
# Install the package in editable mode with development tools.
python -m pip install -e ".[dev]"
# Run all tests.
python -m pytest
Step 8: Run the command-line program
beginner-math
Example Output:
Beginner Math Tools
1. Add
2. Subtract
3. Multiply
4. Divide
Choose an operation: 1
Enter the first number: 8
Enter the second number: 5
Result: 13.0
Step 9: Build the package
# Install the build tool.
python -m pip install --upgrade build
# Create the distribution files.
python -m build
Output:
dist/
├── beginner_math_tools-1.0.0.tar.gz
└── beginner_math_tools-1.0.0-py3-none-any.whl
Explanation: This complete project contains reusable functions, a command-line interface, tests, metadata, a source layout, and build instructions. After testing the generated files, the package could be uploaded to TestPyPI before being considered for publication on the main PyPI service.