"Just cheat sheets for you and your clients."
Well-documented code can save countless hours of debugging and troubleshooting by providing clear guidance and explanation of the code's functionality and usage.
Internal Documentation: Consists of comments and docstrings within the codebase that explain the purpose, logic, and usage of various parts of the program.
External Documentation: Includes README files, documentation of the project functionality, user manuals, and developer guides that describe how to use the software, how it works, and how to contribute to it.
I recommend to write both types of docs to make your app clear as possible either for the user or team.
As you now it already, comments in Python
are preceded by a hash (#
) symbol and should explain the "why" behind a block of code rather than the "how".
- Inline comments are placed on the same line as a statement.
- They should be used to clarify complex pieces of code.
- They should be meaningful and add value to the understanding of the code.
In the bad approach, the comments are redundant, merely repeating what the code does.
x = 5 # Assign 5 to x
y = x + 2 # Add 2 to x and assign to y
The good approach, however, provides a rationale for why the operation is performed, adding value to the code's readability.
x = 5
y = x + 2 # Compensate for border width in calculation
The biggest Python's achievement is that in the majority of cases the code is self-explanationary, but it doesn't mean that we have to forget about inline comments or docstings.
Docstring Conventions and Standards:
Docstrings provide a built-in way of documenting Python modules, functions, classes, and methods. They are enclosed in triple quotes ("""
) and should follow the PEP 257 conventions.
IMPORTANT: A good docstring should describe what the function/class does, its arguments, return values, and any exceptions raised.
Avoid doing this!
def add(a, b):
# Nothing here
return a + b
In the case above we are not really sure which types should we pass to the add
function, that is probably one of the biggest disadvantage.
- Brief Description: A concise summary of what the function does. This should be in the imperative mood (e.g., "Return" instead of "Returns").
- Parameters Section: Lists each parameter name, expected type, and a short description. While the types can also be indicated in the function signature (as of Python 3.5+), repeating them in the docstring can enhance readability and clarity.
- Returns Section: Describes the return value's type and purpose.
Check out the following examples and notice how understandability has been improved significantly.
def add(a, b):
"""
Add two numbers and return the result.
Parameters:
a (int): The first number.
b (int): The second number.
Returns:
int: The sum of a and b.
"""
return a + b
def divide(dividend, divisor):
"""
Divide the dividend by the divisor and return the quotient and remainder.
Parameters:
dividend (int): The number to be divided.
divisor (int): The number by which to divide.
Returns:
tuple: A tuple containing the quotient and remainder (quotient, remainder).
Raises:
ValueError: If the divisor is zero.
"""
if divisor == 0:
raise ValueError("Divisor cannot be zero.")
quotient = dividend // divisor
remainder = dividend % divisor
return quotient, remainder
def find_max(numbers):
"""
Find and return the maximum number in a list.
Parameters:
numbers (list of int): The list of numbers to search.
Returns:
int: The maximum number in the list. Returns None if the list is empty.
Example:
>>> find_max([1, 3, 2])
3
"""
return max(numbers) if numbers else None
Revisit your existing projects and update them with docstrings using right now!
Python's type system, while dynamically typed at runtime, supports optional type hints that enable static type checking. This feature, introduced in Python 3.5 through PEP 484, allows developers to annotate their code with type hints, making it more readable, maintainable, and less prone to errors.
The main idea behind anotations is that we define the expected data type which should be passed to the function/method or the variable.
VERY IMPORTANT: Annotations don't guarantee that exactly this type will be passed, it is not about enforcemnt, but more about documentation.
These are the foundational types that correspond to Python's built-in types:
- int: For integers.
- float: For floating-point numbers.
- bool: For Boolean values (True or False).
- str: For strings.
- bytes: For byte sequences.
name: str = "Alice"
age: int = 30
is_student: bool = True
Composite types, also known as collection types, allow you to specify the type of elements in a collection.
- list[Type]: A list where all elements are of the specified type.
- tuple[Type, ...]: A tuple with specified element types. Use
Tuple[Type, ...]
for variable-length tuples with elements of the same type. - dict[KeyType, ValueType]: A dictionary with specified types for keys and values.
- set[Type]: A set where all elements are of the specified type.
Example:
names: list[str] = ["Alice", "Bob", "Charlie"]
coordinates: tuple[int, int, int] = (10, 20, 30)
student_grades: dict[str, float] = {"Alice": 85.5, "Bob": 92.0}
unique_numbers: set[int] = {1, 2, 3, 4, 5}
Python's typing module also includes more specialized types for more complex scenarios:
- Optional[Type]: Indicates a variable that can be of a specified type or
None
. - Union[Type1, Type2, ...]: Indicates a variable that can be any one of the specified types.
- Callable[[ArgType1, ArgType2, ...], ReturnType]: Represents a callable (function or object with
__call__
) with specified argument and return types. - Any: A special type indicating that the variable can be of any type. Use sparingly, as it essentially opts out of type checking for the variable.
Example:
from typing import Optional, Union, Callable, Any
def add(a: int, b: int) -> int:
return a + b
maybe_number: Optional[int] = None
string_or_number: Union[str, int] = 5
calculator: Callable[[int, int], int] = add
anything: Any = "Hello" # Can be any type
Note: Using Any
is an extremly bad practice which can lead to unpleasent consequences and bugs, if we do some specific operation which are related to specific data types.
Type aliases allow you to define custom types for complex type hints, improving code readability.
Example:
Vector = list[float]
Point = tuple[float, float, float]
velocity: Vector = [1.5, -2.0, 0.0]
location: Point = (10.0, 20.5, 30.0)
Generics: Python's typing module allows for the definition of generic types, making it possible to create container types that can hold objects of any type, specified at runtime. This is particularly useful for classes that act as wrappers or containers for other objects.
Somewhere here, I will refer to word "type checker" which will be covered in the next section. For now, just take a look and try to understand the puprose of these annotations.
from typing import TypeVar, Generic
T = TypeVar('T') # Declare type variable
class Box(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
def get(self) -> T:
return self.item
box = Box[int](123) # Box containing an integer
print(box.get()) # Outputs: 123
NewType: You can create distinct types that are treated as separate types by static type checkers but are runtime-equivalent to their base types. This is useful for adding semantic meaning to base types.
from typing import NewType
UserId = NewType('UserId', int)
def get_user_name(user_id: UserId) -> str:
return "John Doe" # Placeholder for actual lookup
user_id = UserId(5232)
print(get_user_name(user_id))
Literal Types: Literal types indicate that a variable or parameter can only have specific literal values. This is particularly useful when a function accepts a limited set of string or integer values.
from typing import Literal
def handle_status(status: Literal['open', 'closed']) -> None:
print(f"Handling a {status} status")
handle_status('open') # Valid
handle_status('pending') # Type checker will flag this as an error
TypedDict: For dictionaries with a fixed set of keys, where each key has a specific type, TypedDict
offers a way to specify type hints for each key-value pair explicitly.
from typing import TypedDict
class User(TypedDict):
name: str
age: int
user: User = {'name': 'Alice', 'age': 30} # Type checker enforces dict structure
Protocols: Introduced in Python 3.8, Protocols allow for duck typing, defining a set of methods that a class must implement without specifying a specific inheritance hierarchy.
Example:
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None:
...
def close_resource(resource: SupportsClose) -> None:
resource.close()
class MyResource:
def close(self) -> None:
print("Resource closed")
close_resource(MyResource()) # Type checker ensures MyResource has a close method
When it comes to enforcing and checking the type hints in your Python code, Mypy is the go-to tool. Mypy is a static type checker that helps you catch type errors before runtime.
So basically all those annotations which we have added in the previous sections will be valiidated by mypy
. In case of errors or incorrect usage of functions, the type checker will be able to spot them. Thus, developers can prevent bugs with incorrect types provided to the function.
NOTE: Defining types can speed up your code!
-
Installation: Install Mypy with pip:
pip install mypy
-
Basic Usage: To check a single Python file for type errors, run:
mypy your_script.py
Mypy will analyze your code and report any type inconsistencies based on the annotations you've provided.
-
Checking Multiple Files: You can also check multiple files or entire directories by listing them:
mypy file1.py file2.py directory/
-
Configuration: For more complex projects, Mypy can be configured via a
mypy.ini
orpyproject.toml
file in your project's root directory.[mypy] python_version = 3.8 ignore_missing_imports = True
In some cases you would want to run the commands with some of these flags:
--ignore-missing-imports
: Ignores errors about missingimport
statements.--strict
: Enables all of Mypy’s strictness flags for thorough checks.--follow-imports=silent
: Follows import statements but doesn't check the imported modules.--exclude
: Excludes specific files or directories from being checked.
I would recommend to refer to the official documentation for more details. It is a great tool which helps you to ensure that typing is defined correctly across the project.
We won't focuse on this part too much, as it's beyond of the scope of this lesson, but it's great to know that such tool exists.
A README file is often the first document readers encounter in your project. It's not just an introduction; it's your project's handshake with the outside world. A well-crafted README effectively communicates the purpose of your project, how to use it, and how others can contribute.
- Project Name and Logo: Start with the project's name, and if you have one, include a logo.
- Introduction: A brief description of the project and its purpose.
- Installation Instructions: Clear, concise steps to get your project running on another machine.
- Usage: Examples of how to use your project, including code snippets and command-line examples.
- Contributing: Guidelines on how others can contribute to your project, including coding standards, test procedures, and how to submit pull requests.
- License: Information on the project's license, allowing others to understand how they can use, modify, and distribute your work.
- Credits: Acknowledgments of contributors, external resources, or dependencies.
I have been using this template for the whole life and now would love to share it with you! If you have any suggestions, LMK what can be improved.
# Project Name
![Project Logo](path/to/logo.png)
## Introduction
Briefly introduce your project and its goals.
## Installation
Provide a step-by-step guide to install your project:
E.g
git clone https://yourproject.git
cd yourproject
pip install -r requirements.txt
## Usage
Showcase how to use your project with code snippets or command-line examples:
## Contributing
Explain how others can contribute to your project. Include links to issues, coding standards, and contribution guidelines.
## License
State the license under which your project is released, for example, MIT or GPL-3.0.
## Credits
Acknowledge contributors, third-party libraries, and any other resources that helped your project.
You can use either my template to describe your project in README or come up with a new one for your specific needs, again, there is no strict enforcements on the format, just make sure it's consistent, well-written and described in details.
Try to include the following into your docs for the end user:
- Getting Started: A beginner-friendly introduction to the project, highlighting basic functionalities and simple use cases.
- Step-by-Step Guides: Detailed tutorials addressing specific tasks or problems, guiding the user from start to finish.
- FAQs: A list of frequently asked questions (and answers) that users may have when using your project.
NOTE: You will need to find out about hostings and documentation builder utils such as sphinx/mcdocs
and readthedocs
to expose your documentation into the world.
Don't forget to regularly update the documentation to reflect changes in the project. Be up to date!
Update your existing projects with the following requirements:
- Add type annotations to your functions and methods to clearly specify expected input types and return types.
- Ensure all functions, classes, and modules have descriptive docstrings that follow the conventions discussed.
- Create or update a
README.md
file for each project with the following sections and implement internal documentation. - Try writing documentation for the person who will use your application as an end user.