Essential Python Practices for Daily Production Projects
Written on
Chapter 1: Introduction to Python in Production
In this section, we’ll delve into practical aspects of managing Python projects. My aim is to share insights that can significantly enhance your programming journey. Today, I’ll outline my approach to structuring, coding, testing, and deploying a Python project in real-world scenarios.
What do I do?
To provide some background, I work as a Solutions Engineer at a company in Spain. My responsibilities range from developing customized systems to facilitating seamless API integration for new clients. My team utilizes FastAPI for our APIs and Streamlit for a straightforward User Interface (UI).
In this article, I’ll cover:
- My project structuring methods
- Key Python features that can assist you
Let’s jump right in!
How I Structure Python Projects
Creating a new project structure can be daunting. You may find yourself pondering about the initial file to create, the necessary folders, and more. Over time, every programmer develops a unique structuring style that simplifies their workflow. Here’s a list of guidelines I follow:
- Modules and Packages Should Be Intuitive
Each module should have a name that clearly indicates its purpose. A poorly named module can lead to confusion. For example, consider a module named connection.py. What does it connect to? A database? An SSH server? By organizing it into a package labeled db, it’s clear that it pertains to database connections.
my_app
├── __init__.py
└── db
├── __init__.py
└── connection.py
- Maintain Order
I prioritize organization within my code. I always consider how other developers might interpret my work, especially if they need to continue the project in my absence. I want to avoid scenarios where someone might exclaim, "What is this mess?"
As part of my role, I develop APIs that consist of various endpoints. Each endpoint corresponds to a specific action, such as managing Orders or Products.
# main.py
from fastapi import FastAPI # Ensure FastAPI is installed
app = FastAPI()
@app.post('/order')
async def create_order(payload):
# Function to create a new order
pass
@app.patch('/order/{id}')
async def update_order(payload, id):
# Function to update an order
pass
@app.get('/order/{id}')
async def get_order(id):
# Function to retrieve an order by ID
pass
@app.get('/orders')
async def all_orders():
# Function to return all orders
pass
@app.delete('/order/{id}')
async def delete_order(id):
# Function to delete an order by ID
pass
As demonstrated, the list of endpoints can quickly expand if all necessary functions are crammed into a single file. A better approach is to categorize them. I create a routes package to organize endpoints logically.
my_app
├── __init__.py
├── main.py
└── routes
├── __init__.py
└── order.py
└── product.py
For example, all order-related endpoints can reside within order.py:
# routes/order.py
from fastapi import APIRouter
router = APIRouter()
@router.post('/order')
async def create_order(payload):
# Function to create a new order
pass
Now, I can seamlessly integrate this route into my main application:
from fastapi import FastAPI
from routes import order
app = FastAPI()
# Integrate the order routes
app.include_router(order.router)
- Adopt the DRY Principle
Repetitive code can be counterproductive. Striving to eliminate redundancy requires careful planning. The DRY principle emphasizes that every piece of information should have a single, clear representation.
In my project structure, I include a commons package to house shared elements across modules:
my_app
├── __init__.py
├── main.py
└── commons
├── __init__.py
└── base_response.py
Consider a scenario where your project needs to return a standardized response object. Instead of duplicating this structure across different modules, I create a base response object that subsequent classes can inherit.
# commons/base_response.py
class BaseResponse:
def __init__(self):
pass
def status(self, code):
return 200 < code <= 299
- Controllers, Models, and Schemas
You might wonder where the logic for creating products or placing orders resides. Previously, I adhered to the MVC pattern, but I prefer to maintain a more streamlined structure. Each functional area, such as orders, has its dedicated package that includes all relevant models and logic.
my_app
├── order
│ ├── __init__.py
│ └── schemas.py
│ └── response.py # Inherits from base_response.py
- Testing
As expected, I have a tests folder at the root level of the project. I favor using pytest, and each tests folder includes a conftest.py module for shared fixtures.
my_app
├── tests
│ ├── __init__.py
│ └── conftest.py
For more details on testing, stay tuned.
- Managing Project Dependencies
Every project requires specific dependencies to function correctly. These can include:
- A requirements.txt file that lists all Python dependencies
- A settings.py file for project configurations
- A Dockerfile for containerization
- A Makefile for commands and configurations
my_app
├── settings.py
├── Dockerfile
├── Makefile
├── README.md
├── requirements.txt
Chapter 2: Key Python Features for Enhanced Programming
Now, let’s explore some Python features that can enhance your programming experience.
- Utilizing Match/Case
Introduced in Python 3.10, the match statement offers a cleaner alternative to complex conditional structures. It can streamline your code, particularly when faced with multiple if-elif statements.
match expression:
case a:
# Handle case acase b:
# Handle case bcase _:
# Default case
- Generic Types with Typing
Generic types in Python allow for more flexible and reusable code. They can be particularly useful when you are unsure of the data type in advance.
# base_response.py
class BaseResponse:
def another_method(self) -> T:
# Return a generic type
pass
- @override Decorator
In my examples, I demonstrated a base response class that can be extended. To clarify when methods are being overridden, I utilize the @override decorator from PEP 698.
from typing import override
from commons.base_response import BaseResponse
class Response(BaseResponse):
@override
def another_method(self):
# Custom implementation
pass
Final Thoughts
Every project is an opportunity to learn and apply new skills. While starting a new project can be intimidating, that feeling dissipates as I dive into organizing my work.
Structuring your projects is akin to tidying up your living space. Everyone has their methods, but the goal remains the same: to create an organized and clean environment so that you or anyone else can easily navigate through it.
Thank you for taking the time to read my article! If you found this helpful and would like to receive similar content, consider subscribing to the community.
This video showcases five quick Python projects suitable for beginners, all of which can be completed in a single day.
This video provides insights into program structure as part of the Python For Production series, offering free lessons from a professional developer.