10 Python Coding Tips They Don’t Teach in Tutorials

Wiktoria Kasprzak
5 min readNov 16, 2024

--

When I started my coding journey, learning Python was all about syntax, algorithms, and getting things to work. What many tutorials didn’t teach, however, were the habits that make code sustainable and collaborative — the kind of practices that save hours down the road and allow teammates to build on your work.

For real-world teams, the ability to maintain, scale, and collaborate on code is critical. Sustainable code isn’t just about solving a problem today; it’s about making sure that your work can be understood, extended, and improved by others tomorrow. Whether you’re a solo developer or part of a team, adopting best practices from the start ensures that your code is both reliable and easy to manage.

This article covers 10 essential habits that help you write Python code that stands the test of time, allowing others to build on your work without frustration. These are the practices I wish I had learned early in my career, and they’ll make your coding life easier in the long run.

Sereti, N. Lines of code · Free Stock Photo, Blurry Photo of Lines of Code on Screen. Available at: https://www.pexels.com/photo/lines-of-code-2653362/.

1. Use Virtual Environments

A virtual environment isolates your project’s dependencies, preventing conflicts when working on multiple projects. If you’re not using tools like venv or conda, you’re setting yourself up for compatibility headaches.

To quickly create and activate a virtual environment using venv, run the following in your terminal:

# Create the virtual environment, where 'env' is the name of your environment
python -m venv env

# Activate it (Windows)
env\Scripts\activate

# Activate it (Mac/Linux)
source env/bin/activate

2. Write Docstrings and Use Typing

Well-documented functions with clear type hints make your code easier to understand and maintain. Docstrings should briefly explain what a function does, what it expects, and what it returns. Type hints improve clarity and help with debugging.

Example:

def calculate_mean(values: list[float]) -> float:  
"""
Calculate the mean of a list of numbers.
Args:
values: A list of numbers for the calculation.
Returns:
The mean value.
"""
return sum(values) / len(values)

3. Use a Linter

Consistency in style makes your code more readable. Linters like Black or pylint help enforce standards effortlessly. Linters catch common mistakes, enforce formatting rules, and ensure your codebase looks clean and professional. They also integrate well with most IDE’s, providing real-time feedback as you write code.

4. Question the Need for Classes

Not every problem requires object-oriented design. Sometimes, a few well-written functions are cleaner, easier to debug, and quicker to implement than a class. Personally, I prefer a functional programming approach for most tasks, as it keeps the code simple and focused.

That said, I find classes useful for defining structured data types, especially when paired with tools like @dataclass and Enum.

Example:

from dataclasses import dataclass
from enum import Enum

class UserRole(Enum):
"""Enum representing different user roles."""
ADMIN = "Admin"
USER = "User"
GUEST = "Guest"

@dataclass
class Person:
"""A class representing a person with a name, address, role, and active status."""
name: str
address: str
role: UserRole # Explicitly typed as UserRole
active: bool = True

# Example usage
person: Person = Person(name="Jane Doe", address="123 Main St", role=UserRole.ADMIN)

Using @dataclass simplifies the creation of data containers, while Enum ensures type safety and readability for fixed sets of values. This approach keeps the code organised and expressive.

Classes can be beneficial when managing complex states or representing objects with multiple behaviours and properties. However, they can also introduce unnecessary complexity if overused. Always consider whether the structure a class brings is justified by your code’s requirements.

5. Structure Your Code with a main Function

A main function defines the starting point of your program, making it easy to understand where the execution begins. This structure also allows parts of your code to be reused as modules in other programs.

Example:

import logging

# Configure logging
logging.basicConfig(level=logging.INFO)

def greet_user() -> None:
"""
Log a greeting message.

Returns:
None.
"""
logging.info("Welcome to the program!")

def process_data(data: list) -> list:
"""
Dummy function to simulate data processing.

Args:
data (list): A list of items to process.

Returns:
list: Processed data (in this case, the same list).
"""
logging.info(f"Processing {len(data)} items...")
return data

def main() -> None:
"""
Entry point of the program that greets the user and processes data.

Returns:
None.
"""
greet_user()
data = [1, 2, 3, 4, 5]
processed_data = process_data(data)
logging.info(f"Processed data: {processed_data}")

if __name__ == "__main__":
main()

This structure is a good practice, especially for larger projects, as it improves readability and modularity.

6. Avoid Data Types in Variable Names

Good variable names focus on intent, not structure. Instead of list_of_cat_names, use cat_names. The type should be apparent from the context or typing.

7. Use Logging, Not Print Statements

Print statements are useful during development for quick debugging, but they’re not suitable for production environments. They can clutter the console and don’t offer much control over message severity or log output. Logging, on the other hand, provides a flexible way to manage messages at different levels (e.g., INFO, DEBUG, ERROR) and includes timestamps, which are essential for diagnosing issues in a scalable and maintainable way.

Here’s a simple example to demonstrate logging:

import logging
from typing import List

# Configure logging to display INFO level messages and above
logging.basicConfig(level=logging.INFO)

# Log an informational message
logging.info("Program started.")

def process_data(data: List[int]) -> List[int]:
"""
Processes a list of integers and returns the processed data.

Args:
data (List[int]): A list of integers to be processed.

Returns:
List[int]: The processed list of integers.
"""
logging.debug(f"Processing {len(data)} items...") # DEBUG level
return data

# Simulate data processing
data = [1, 2, 3, 4]
processed_data = process_data(data)

# Log a final message
logging.info(f"Processed data: {processed_data}")

8. Document Your Project with a README

A README explains what your code does, how to set it up, and how to run it. It’s your project’s introduction to the world. If you’re unsure where to start, there are plenty of templates available on GitHub to guide you.

9. Include a requirements.txt File with Versions

Pinning library versions ensures that others can reproduce your environment without surprises caused by updates. Generate one easily with:

pip freeze > requirements.txt

10. Use a Style Guide Across Your Team

A consistent style ensures that your codebase looks like it was written by one person, not a team of many. It makes reading and maintaining code much easier. I recommend the Google Python Style Guide. It covers everything from naming conventions to comment formatting, ensuring consistency across your team.

Why It Matters

These practices may not be flashy, but they transform your code from something that just works to something maintainable, scalable, and professional.

They save time in debugging, make onboarding teammates smoother, and ensure the longevity of your projects.

Not every habit will fit every workflow, so consider what works best for your team and projects. If you’re a beginner or looking to level up, start experimenting with these tips. You’ll thank yourself later — and so will your team.

What other coding habits have made a big difference in your workflow?

--

--

Responses (1)