Type Hints and Docstrings¶
This document defines the conventions for type hints and docstrings in the Ptera Software codebase.
Table of Contents¶
Type Hints¶
General Principles¶
Type hints should reflect what functions ACCEPT, not just what they store internally
Always include return type hints (use
-> Nonefor functions that don’t return values)Shape information belongs in docstrings, not type hints (Python’s type system can’t express array shapes)
Import Requirements¶
from collections.abc import Sequence
import numpy as np
Type Hint Patterns by Parameter Type¶
Basic Types¶
Parameter Description |
Type Hint |
|---|---|
String |
|
Boolean |
|
Boolean (accepting numpy bools) |
|
Integer |
|
Number (int or float) |
|
Float only |
|
Array and Array-Like Types¶
Parameter Description |
Type Hint |
Notes |
|---|---|---|
Array-like accepting numbers (int or float) |
|
For user-facing parameters that accept tuples/lists/arrays |
Array-like accepting floats only |
|
When only floats are valid |
Array of arrays (2D array-like) |
|
For nested sequences like |
Already a numpy array |
|
For internal functions or returns |
Class Types¶
Parameter Description |
Type Hint |
Notes |
|---|---|---|
Class from same package |
|
Direct reference |
Class from imported module |
|
Use module alias to avoid circular imports |
Optional and Union Types¶
Parameter Description |
Type Hint |
|---|---|
Optional parameter |
|
Union of multiple types |
|
Type Narrowing Patterns¶
When working with attributes that may be None, use these patterns:
Optional Attributes¶
For class attributes initialized to None but populated later:
class Solver:
def __init__(self):
# Attribute that will be populated before use
self.current_airplanes: list[geometry.airplane.Airplane] | None = None
Narrowing with Assertions¶
Use assert when you have a programming invariant (the value should never be None at this point):
def compute_forces(self):
# At this point in the code, these should always be populated
assert self.current_airplanes is not None
for airplane in self.current_airplanes:
# mypy now knows current_airplanes is not None
...
Use assert when:
None represents a bug, not a valid state
You want runtime safety during development
The invariant should always hold
Narrowing with cast()¶
Use cast() sparingly, only when the type checker cannot infer what you know to be true:
from typing import cast
# For dtype=object arrays where we know the element type
ring_vortex = cast(_vortices.ring_vortex.RingVortex, object_array[i, j], )
Use cast() when:
Working around type checker limitations (e.g., numpy dtype=object arrays)
You’re certain of the type but can’t prove it to the type checker
No runtime check is needed
Avoid cast() for Type | None → Type narrowing - use assert instead for runtime safety.
Module Alias Pattern¶
Import modules with aliases:
from . import airfoil as airfoil_mod
from . import wing as wing_mod
from . import wing_cross_section as wing_cross_section_mod
# In function signature
def mesh_wing(wing: wing_mod.Wing) -> None:
...
def _get_mcl_points(
inner_airfoil: airfoil_mod.Airfoil,
outer_airfoil: airfoil_mod.Airfoil,
...
) -> list[np.ndarray]:
...
Avoiding Circular Imports with Type Hints¶
Preferred Method: from __future__ import annotations¶
To avoid circular import errors when type hinting, use from __future__ import annotations as the first import in your module. This defers evaluation of type hints, treating them as strings automatically:
from __future__ import annotations
from collections.abc import Sequence
import numpy as np
from . import wing_movement as wing_movement_mod
from .. import geometry
# In function signature - no quotes needed!
def __init__(
self,
base_airplane: geometry.airplane.Airplane,
wing_movements: list[wing_movement_mod.WingMovement],
) -> None:
...
This approach:
Keeps all imports at the top of the file
Prevents circular import errors
Requires no string quotes around type hints
Is the default behavior in Python 3.11+
Docstring Format¶
General Principles¶
Use reStructuredText (rST) format
Short description starts on same line as triple quotes
Parameter descriptions are inline (no separate type line)
Begin descriptions with article + shape/type info for arrays (e.g., “A (4,4) ndarray of floats…”)
Use present tense for descriptions (e.g., “Returns…” not “Will return…”)
Avoid starting descriptions with “This…”
Never use em-dashes (—) or en-dashes (–); always use hyphens (-)
Never use multiplication sign (×); use lowercase x
Never use pi symbol (π); write “pi”
Never use approximately-equal sign (≈); use “~”
Place closing triple-quotes on their own line
Module-Level Docstrings¶
Module-level docstrings appear at the very top of each Python file and describe the module’s contents. The style varies based on the type of module.
Public Package init.py Files¶
__init__.py files for a public package list subpackages, directories, and modules:
"""Contains the geometry classes.
**Contains the following subpackages:**
None
**Contains the following directories:**
None
**Contains the following modules:**
airfoil.py: Contains the Airfoil class.
airplane.py: Contains the Airplane class.
wing.py: Contains the Wing class.
wing_cross_section.py: Contains the WingCrossSection class.
"""
Pattern:
Brief description using “Contains” (present tense)
List public subpackages (or “None”)
List public directories (or “None”)
List public modules with one-line descriptions
Use blank lines between subpackages/directories/module entries for readability
Public Modules¶
Public modules (e.g., airfoil.py, wing.py) have structured docstrings listing their contents:
"""Contains the Airfoil class.
**Contains the following classes:**
Airfoil: A class used to contain the Airfoil of a WingCrossSection.
**Contains the following functions:**
None
"""
Pattern:
Brief description using “Contains” (present tense)
List public classes with brief descriptions (use “A class used to…” or similar)
List public functions (or “None”)
Use blank lines between class/function entries if there are multiple
Private Modules¶
Private modules (e.g., _meshing.py, _functions.py) have minimal docstrings:
"""Contains the function for meshing Wings."""
Pattern:
Single brief sentence
No listing of functions or classes
Keep it concise since these are internal implementation details
Function and Method Docstrings¶
def function_name(
param1: Type1,
param2: Type2,
param3: Type3,
) -> ReturnType:
"""Short description of what the function does.
Optional longer description providing more context. This provides detailed
explanations of the function's behavior. It can be one or more paragraphs.
Optional citation block:
**Citation:**
Adapted from (can be more specific if the whole function wasn't adapted): <source>
Author (or "Authors"): <author>
Date of retrieval (don't include if not known): <date>
:param param1: A (shape) dtype description of param1. Additional details about
what it represents, valid ranges, units, etc. Can wrap to multiple lines.
:param param2: Description of param2.
:param param3: Description of param3.
:return: A (shape) dtype description of what is returned. Additional details
about the return value.
"""
Array Parameter Descriptions¶
For numpy arrays, always include:
Shape: e.g., “(4,4)”, “(M,N,3)”, “(N,)”
Dtype: e.g., “ndarray of floats”, “ndarray of ints”, “ndarray of bools”
Coordinate system and reference point (when applicable)
Units (when applicable)
Default value (when applicable)
Pattern for Array Parameters¶
:param parameter_name: A (shape) ndarray of dtype representing <description>.
Additional context about coordinate systems, valid ranges, units, default value, etc.
Pattern for Array-Like Parameters¶
:param parameter_name: An array-like object of numbers (int or float) with shape
(N,M) representing <description>. Can be a tuple, list, or ndarray. Values are
converted to floats internally. The units are <units>. The default is <default>.
Class Docstrings¶
class ClassName:
"""Short description of the class.
**Contains the following methods:**
public_method_1: Short description (identical to method's docstring's short
description.
public_method_2: Short description (identical to method's docstring's short
description.
Optional notes block
**Notes:**
Detailed description of the class's purpose, behavior, or usage. Can be one or more
paragraphs. Avoid numbered or bulleted lists.
Optional citation block:
**Citation:**
Adapted from (can be more specific if the whole class wasn't adapted): <source>
Author (or "Authors"): <author>
Date of retrieval (don't include if not known): <date>
"""
Subclass Docstrings¶
When a class inherits from another class, use a modified pattern that avoids duplicating documentation from the parent class.
Subclass Class Docstring Template¶
class ChildClass(ParentClass):
"""A subclass of ParentClass used to <description>.
**Notes:**
Inherits all parameters and methods from ParentClass without modification.
Additional notes specific to the subclass. Can be one or more paragraphs.
**Contains the following methods:**
new_method_1: Short description of new method (if any).
None (if no new methods are added)
"""
Key points:
Short description explicitly mentions “A subclass of ParentClass”
Notes section states what is inherited from the parent
“Contains the following methods:” lists only NEW methods unique to this subclass
Write “None” if no new methods are added
Subclass __init__ Docstring Template¶
def __init__(
self,
inherited_param1: Type1,
inherited_param2: Type2,
new_param: Type3,
) -> None:
"""The initialization method.
See ParentClass's initialization method for descriptions of inherited
parameters.
:param new_param: Description of the new parameter unique to this subclass.
:return: None
"""
super().__init__(inherited_param1, inherited_param2)
self.new_param = new_param
Key points:
Reference the parent class’s
__init__docstring for inherited parametersOnly document parameters that are NEW to the subclass
Call
super().__init__()with inherited parameters
Property Docstring Template¶
@property
def property_name(self) -> ReturnType:
"""Short description of what the property represents.
:return: Description of what is returned, including type, shape, units.
"""
For simple getter properties that just return a stored attribute, the docstring can be omitted if the attribute is already thoroughly documented in __init__’s docstring. This applies broadly to all simple getter properties, not only to cached properties with invalidating setters.
Cached Properties with Invalidating Setters¶
When implementing a caching pattern where computed properties are lazily evaluated and cached, with setters that invalidate dependent caches, follow these conventions:
Property Getters for Cached Attributes¶
For simple getters that return a cached value (e.g., corner point positions that were previously plain attributes), use a brief docstring or omit the docstring entirely if the attribute is already thoroughly documented in __init__:
@property
def Frpp_G_Cg(self) -> np.ndarray:
# No docstring as this attribute is documented in __init__()'s docstring
return self._Frpp_G_Cg
@property
def Frpp_GP1_CgP1(self) -> np.ndarray:
"""The position of the Panel's front right vertex (in the first Airplane's geometry
axes, relative to the first Airplane's CG).
:return: A (3,) ndarray of floats representing the position of the Panel's front
right vertex (in the first Airplane's geometry axes, relative to the first
Airplane's CG). The units are in meters. Returns None if not yet set or if
Frpp_G_Cg has been modified since last set.
"""
return self._Frpp_GP1_CgP1
Property Setters that Invalidate Caches¶
Do not add docstrings to setters. The cache invalidation behavior is an implementation detail.
@Frpp_G_Cg.setter
def Frpp_G_Cg(self, newFrpp_G_Cg: np.ndarray) -> None:
# No docstring as this is a setter method
self._rightLeg_G = None
self._frontLeg_G = None
self._Frbvp_G_Cg = None
self._Cpp_G_Cg = None
self._unitNormal_G = None
self._area = None
self._aspect_ratio = None
self.Frpp_GP1_CgP1 = None
self._Frpp_G_Cg = newFrpp_G_Cg
Cached Computed Properties¶
For computed properties that are now cached (e.g., rightLeg_G, area), the existing docstring remains unchanged. Caching is an implementation detail that does not affect the public interface.
Class Docstring for Classes with Caching¶
When a class uses this caching pattern, add a Notes: section to the class docstring explaining the caching behavior. Don’t add the getters and setter methods for the non-computed properties to the list of methods:
class Panel:
"""A class used to contain the panels of a Wing.
**Contains the following methods:**
rightLeg_G: The Panel's right leg vector (in geometry axes).
area: An estimate of the Panel's area.
[... other methods (don't include Frpp_G_Cg or Frpp_GP1_CgP1 ...]
**Notes:**
Computed geometric properties (leg vectors, bound vortex points, collocation points,
unit normals, area, and aspect ratio) are lazily evaluated and cached. Setting any
corner point position invalidates all dependent cached values, ensuring consistency
while avoiding redundant computation. Setting a corner point's local position
(one of the parameters with a _G_Cg suffix), sets the corresponding global position
(_GP1_CgP1 suffix) to None. It also sets this Panel's bound vortices and the loads
on the Panel to None.
"""
Examples¶
Example 1: Module-Level Docstrings¶
Public Package init.py¶
"""Contains the geometry classes.
**Contains the following subpackages:**
None
**Contains the following directories:**
None
**Contains the following modules:**
airfoil.py: Contains the Airfoil class.
airplane.py: Contains the Airplane class.
wing.py: Contains the Wing class.
wing_cross_section.py: Contains the WingCrossSection class.
"""
Public Module¶
"""Contains the Airfoil class.
**Contains the following classes:**
Airfoil: A class used to contain the Airfoil of a WingCrossSection.
**Contains the following functions:**
None
"""
Private Module¶
"""Contains the function for meshing Wings."""
Example 2: Function with Array Parameters (Internal)¶
def _get_mcl_points(
inner_airfoil: airfoil_mod.Airfoil,
outer_airfoil: airfoil_mod.Airfoil,
chordwise_coordinates: np.ndarray,
) -> list[np.ndarray]:
"""Takes in the inner and outer Airfoils of a wing section and its normalized
chordwise coordinates. It returns a list of four column vectors containing the
normalized components of the positions of points along the mean camber line (MCL)
(in each Airfoil's axes, relative to each Airfoil's leading point).
:param inner_airfoil: The wing section's inner Airfoil.
:param outer_airfoil: The wing section's outer Airfoil.
:param chordwise_coordinates: A (N,) ndarray of floats for the normalized
chordwise coordinates where we'd like to sample each Airfoil's MCL. The values
are normalized from 0.0 to 1.0 and are unitless.
:return: A list of four (N,1) ndarrays of floats, where N is the number of points
at which we'd like to sample each Airfoil's MCL. The ndarrays contain components
of the positions of points along each Airfoil's MCL. In order, the ndarrays
returned are, (1) the inner Airfoil's MCL points' y-components, (2) the inner
Airfoil's MCL points' x-components (3) the outer Airfoil's MCL points'
y-components, and (4) the outer Airfoil's MCL points' x-components. The values
are normalized from 0.0 to 1.0 and are unitless.
"""
Example 2: Function with Transformation Matrices¶
def _get_mcs_points(
T_pas_Wcsi_Lpi_Wn_Ler: np.ndarray,
T_pas_Wcso_Lpo_Wn_Ler: np.ndarray,
inner_wing_cross_section: wing_cross_section_mod.WingCrossSection,
outer_wing_cross_section: wing_cross_section_mod.WingCrossSection,
inner_mcl_pointsY_Ai_lpAi: np.ndarray,
inner_mcl_pointsX_Ai_lpAi: np.ndarray,
outer_mcl_pointsY_Ao_lpAo: np.ndarray,
outer_mcl_pointsX_Ao_lpAo: np.ndarray,
spanwise_coordinates: np.ndarray,
) -> list[np.ndarray]:
"""Calculates the points on a wing section's mean camber surface (MCS) (in wing
axes, relative to the leading edge root point).
:param T_pas_Wcsi_Lpi_Wn_Ler: A (4,4) ndarray of floats representing a passive
transformation matrix which maps in homogeneous coordinates from the inner
WingCrossSection's axes, relative to its leading point to wing axes relative to
the leading edge root point.
:param T_pas_Wcso_Lpo_Wn_Ler: A (4,4) ndarray of floats representing a passive
transformation matrix which maps in homogeneous coordinates from the outer
WingCrossSection's axes, relative to its leading point to wing axes relative to
the leading edge root point.
:param inner_wing_cross_section: The wing section's inner WingCrossSection.
:param outer_wing_cross_section: The wing section's outer WingCrossSection.
:param inner_mcl_pointsY_Ai_lpAi: A (M,1) ndarray of floats, where M is the
number of chordwise points in the mesh. Each element represents the y-component
of the inner Airfoil's MCL points (in the inner Airfoil's axes, relative to the
inner Airfoil's leading point). The values are normalized from 0.0 to 1.0 and
are unitless.
:return: A list of four (M,N,3) ndarrays of floats, where M is the number of
chordwise points and N is the number of spanwise points. The four ndarrays are,
in order, this wing section's Panel's (1) forward inner, (2) forward outer,
(3) backward inner, and (4) backward outer panel points (in wing axes, relative
to the leading edge root point). The units are in meters.
"""
Example 3: Public Method with Array-Like Parameters¶
def __init__(
self,
name: str = "NACA0012",
outline_A_lp: np.ndarray | Sequence[Sequence[float | int]] | None = None,
resample: bool = True,
n_points_per_side: int = 400,
) -> None:
"""The initialization method.
:param name: The name of the Airfoil. It should correspond to the name of a file
the airfoils directory, or to a valid NACA 4-series airfoil (once converted to
lower-case and stripped of leading and trailing whitespace) unless you are
passing in your own array of points using outline_A_lp. Note that NACA0000 isn't
a valid NACA-series airfoil. The default is "NACA0012".
:param outline_A_lp: An array-like object of numbers (int or float) with shape
(N,2) representing the 2D points making up the Airfoil's outline (in airfoil
axes, relative to the leading point). If you wish to load coordinates from the
airfoils directory, leave this as None, which is the default. Can be a tuple,
list, or ndarray. Values are converted to floats internally. Make sure all
x-component values are in the range [0.0, 1.0]. The default value is None.
:param resample: Determines whether to resample the points defining the Airfoil's
outline. This applies to points passed in by the user or to those from the
airfoils directory. I highly recommended setting this to True. Can be a bool or
a numpy bool and will be converted internally to a bool. The default is True.
:param n_points_per_side: The number of points to use when creating the Airfoil's
MCL and when resampling the upper and lower parts of the Airfoil's outline. It
must be a positive int greater than or equal to 3. The resampled outline will
have a total number of points equal to (2 * n_points_per_side) - 1. I highly
recommend setting this to at least 100. The default value is 400.
:return: None
"""
Example 4: Method Returning Self-Reference¶
def add_control_surface(
self, deflection: float | int, hinge_point: float | int
) -> Airfoil:
"""Returns a version of the Airfoil with a control surface added at a given point.
It is called during meshing.
:param deflection: The control deflection in degrees. Deflection downwards is
positive. It must be a number (int or float) in the range [-5.0, 5.0] degrees.
Values are converted to floats internally.
:param hinge_point: The location of the hinge as a fraction of chord length. It
must be a number (int or float) in the range (0.0, 1.0). Values are converted
to floats internally.
:return: The new Airfoil with the control surface added.
"""
Example 5: Method with Optional Return¶
def get_plottable_data(self, show: bool = False) -> list[np.ndarray] | None:
"""Returns plottable data for this Airfoil's outline and mean camber line.
:param show: Determines whether to display the plot. Can be a bool or a numpy bool,
and will be converted internally to a bool. If True, the method displays the
plot and returns None. If False, the method returns the data without displaying.
The default is False.
:return: A list of two ndarrays containing the outline and MCL data, or None if
show is True.
"""
Example 6: Method Returning Array¶
def get_resampled_mcl(
self, mcl_fractions: np.ndarray | Sequence[float]
) -> np.ndarray:
"""Returns a ndarray of points along the mean camber line (MCL), resampled from the
mcl_A_outline attribute. It is used to discretize the MCL for meshing.
:param mcl_fractions: A (N,) array-like object of floats representing normalized
distances along the MCL (from the leading to the trailing edge) at which to
return the resampled MCL points. Can be a tuple, list, or ndarray. The first
value must be 0.0, the last must be 1.0, and the remaining must be in the range
[0.0, 1.0]. All values must be non duplicated and in ascending order.
:return: A (N,2) ndarray of floats that contains the positions of the resampled
MCL points (in airfoil axes, relative to the leading point).
"""
Quick Reference¶
Common Type Hint Patterns¶
# Simple types
param: str
param: bool
param: bool | np.bool_ # Accepts both Python and numpy bools
param: int
param: float | int
# Array-like (user input)
param: np.ndarray | Sequence[float | int]
param: np.ndarray | Sequence[float]
param: np.ndarray | Sequence[Sequence[float | int]]
# Already numpy arrays
param: np.ndarray
# Classes
param: ClassName # Same module
param: module_alias.ClassName # Different module
-> "ClassName" # Self-reference
# Optional/Union
param: Type | None
param: Type1 | Type2
# Collections
-> list[np.ndarray]
-> list[ClassName]
Common Docstring Phrases¶
# Module level
"Contains the <description>."
"Contains the following subpackages:"
"Contains the following directories:"
"Contains the following modules:"
"Contains the following classes:"
"Contains the following functions:"
# Array parameters
":param name: A (shape) ndarray of dtype representing..."
":param name: An array-like object of numbers (int or float) with shape..."
# Boolean parameters (accepting numpy bools)
":param name: A bool that... Can be a bool or a numpy bool and will be converted internally to a bool."
# Return values
":return: A (shape) ndarray of dtype that..."
":return: A list of N (shape) ndarrays..."
":return: None"
# Function descriptions (avoid "This function/method")
"Takes in... and returns..."
"Calculates..."
"Returns..."
"Validates..."
# Units and ranges
"The units are meters."
"The values are normalized from 0.0 to 1.0 and are unitless."
"It must be in the range [0.0, 1.0]."
"It must be a positive int."
# Coordinate systems
"(in geometry axes, relative to the CG)"
"(in wing axes, relative to the leading edge root point)"
"(in airfoil axes, relative to the leading point)"
Notes¶
This style guide should be updated as new patterns emerge
All existing code should gradually be updated to match this style
Use
docformatteror similar tools to help maintain consistent formattingShape information is critical and must always be included in docstrings for arrays
Avoid using hyphens or other forms of dashes in docstrings or comments. This is because they are often incorrectly wrapped by docformatter and incorrectly rendered in PyCharm’s quick documentation. For example, even though not standard grammar, it’s okay to write “non symmetric” instead of “non-symmetric”.