Skip to main content
This document outlines the Python coding standards and conventions used across Muna.

General Principles

  • Readability: Code should be clear and self-documenting.
  • Consistency: Follow established patterns throughout the codebase.
  • Type Safety: Use comprehensive type annotations.
  • Modularity: Keep functions and classes focused on single responsibilities.
  • Performance: Consider efficiency in compiler-related code.

File Structure and Organization

This section covers how to structure Python files, organize imports, and arrange code within modules to maintain clarity and consistency.
All Python files must start with the standard copyright header:
#
#   Muna
#   Copyright © 2025 NatML Inc. All Rights Reserved.
#
Imports should be organized in the following order:
# 1. Future imports
from __future__ import annotations

# 2. Standard library imports (alphabetical)
from abc import ABC, abstractmethod
from collections.abc import Callable
from pathlib import Path

# 3. Third-party imports (alphabetical)
from pydantic import BaseModel
from yaml import safe_load

# 4. Local/project imports (alphabetical, relative imports last)
from .schema import Operator
Prefer structuring modules sequentially:
# 1. Copyright header
# 2. Future imports
# 3. Other imports
# 4. Module-level constants
# 5. Exception classes
# 6. Data classes/models
# 7. Function definitions
# 8. Class definitions
# 9. Main execution block (if applicable)

Imports

We use specific imports rather than wildcards, always include from __future__ import annotations when needed, and organize imports in four distinct groups with alphabetical ordering within each group. Related imports are grouped together, and we prefer absolute imports for clarity.
  • Use from __future__ import annotations at the top of files that use forward references
  • Prefer specific imports over wildcard imports
  • Group related imports together
  • Use absolute imports for clarity
Example:
# Good: Absolute imports
from __future__ import annotations
from pathlib import Path
from typing import Iterator

# Good: Relative imports
from ..foo import SomeType, AnotherType
from .schema import SomeModel

# Bad
from pathlib import *
from ..foo import *

Naming Conventions

We use snake_case for variables and functions, PascalCase for classes, UPPER_SNAKE_CASE for constants, and single leading underscores for private members. Names should be descriptive and avoid abbreviations unless they’re well-known in the domain.
  • Use snake_case for variables, functions, and module names
  • Use descriptive names that clearly indicate purpose
  • Avoid abbreviations unless they’re well-known
# Good
def get_function_signature(func: Callable[..., object]) -> Signature:
    ...

# Avoid
def get_func_sig(f):
    ...
  • Use PascalCase for class names
  • Use descriptive names that indicate the class purpose
class CompileError(Exception):
    pass
  • Use UPPER_SNAKE_CASE for module-level constants
  • Group related constants together
SDK_VERSION = "2.34.0.250424"
GOOGLE_URL = f"https://www.google.com"
  • Use single leading underscore for internal functions/methods
  • Use double leading underscore for name mangling when necessary
def _generate_sample_data(count: int) -> list[SampleItem]:
    pass

class _TensorSpec(BaseModel):
    pass

Function and Class Definitions

We requrie comprehensive type annotations on functions and classes. We use keyword-only arguments for optional parameters, and group logically.
  • Always include type annotations for parameters and return values
  • Use keyword-only arguments for optional parameters when appropriate
  • Group related parameters together
# Good: Fully annotate parameter and return types
def process_data(items: list[str]) -> list[str]:
    return items

# Good: Use keyword-only arguments for optional parameters when appropriate
def compile(
    source: SourceCode,
    *,
    src_dir: Path=Path("src"),
    build_dir: Path=Path("build"),
    config: BuildConfig="MinSizeRel",
) -> CompiledCode | None:
  • Inherit from appropriate base classes (ABC, BaseModel, etc.)
  • Use abstract methods when defining interfaces
  • Include type annotations for class attributes
# Good: Inherit from an appropriate base class
class Database(ABC):

    # Good: Use abstract methods when defining interfaces
    @abstractmethod
    def search(
        self,
        query: str,
        *,
        limit: int = 10
    ) -> list[Item]:
        """
        Search for items given a query
        """
        pass

Type Annotations

We use comprehensive type annotations throughout the codebase. All function parameters, return types, and class attributes must be annotated.
Use built-in types and typing module for annotations:
from typing import Iterator, Literal
from collections.abc import Callable

def process_items(items: list[str]) -> Iterator[str]:
    pass

def create_model(backend: Literal["cpu", "gpu", "htp"]) -> Model:
    pass
Use the pipe | syntax for union types (Python 3.10+):
def get_result() -> str | None:
    pass

def process_value(value: int | float) -> float:
    pass
Make sure to specify type arguments for generic types:
# Good: Specify generic type arguments
def sum(value: list[int]) -> int:
    return value

# Bad: No generic type arguments for generic type
def sum(value: list) -> int:
    return value
Prefer using from __future__ import annotations for forward references over string annotations:
from __future__ import annotations

class Node:

    # Good: Use type annotation
    def __init__(self, parent: Node | None = None):
        self.parent = parent

    # Bad: Using string annotations
    def __init__(self, parent: "Node" | None = None):
        self.parent = parent

Comments and Docstrings

We encourage the use of comments and docstrings to provide context around code.
Use triple-quoted strings for docstrings. Follow Google-style format:
def detect_objects(
    image: Image.Image
    *,
    min_score: float=0.3,
    max_iou: float=0.5
) -> list[Detection]:
    """
    Detect objects in an image.

    Parameters:
        image (PIL.Image): Input image
        min_score (float): Minimum detection score.
        max_iou (float): Maximum IoU.

    Returns:
        list: Detected objects.
    """
  • Use comments sparingly for complex logic
  • Prefer self-documenting code over comments
  • Use # CHECK for areas that need review
  • Use # INCOMPLETE for unfinished functionality
# CHECK # This might have issues with complex nested structures
if isinstance(value, dict):
    process_dict(value)

# INCOMPLETE # Add support for additional tensor types
tensor_type = get_basic_tensor_type(input_tensor)

Code Formatting

We write highly ergonomic code that is easy to both look at and reason about:
  • Maximum line length: 120 characters
  • Break long lines using parentheses for natural grouping
# Good: Long list declarations over multiple lines
shell_args = [
    f"echo",
    f"'Hello world'",
]

# Good: Long function calls over multiple lines
result = some_long_function_name(
    first_argument,
    second_argument,
    third_argument
)
  • Use 4 spaces for indentation
  • No trailing whitespace
  • Single blank line between function and class definitions
  • No spaces around = in keyword arguments
def function_one():
    pass

def function_two():
    pass

class FirstClass:
    pass

class SecondClass:
    pass
  • Use comments to separate logical blocks within functions or methods instead of empty lines:
def process_model(model_path: Path) -> ProcessedModel:
    # Load and validate model
    model = load_model(model_path)
    validate_model_format(model)
    # Process model layers
    layers = extract_layers(model)
    optimized_layers = optimize_layers(layers)
    # Generate output
    processed_model = create_processed_model(optimized_layers)
    return processed_model
  • Use f-strings for string interpolation
  • Use double quotes for strings consistently
  • Use triple double quotes for multiline strings
# Use double quotes for string literals
greeting = "Hello and have a nice day!"

# Use F-strings for string interpolation
sql_query = f"SELECT * FROM {table_name} WHERE id = {item_id}"

# Use triple double quotes for multiline strings
template = """
This is a multiline
string template with {variable}.
"""

Error Handling

We encourage defining a small set of custom exception types that can be explicitly handled:
  • Create specific exception classes for different error types:
class CompileError(Exception):
    """
    Compile error.
    """
  • Provide clear, actionable error messages
  • Include context about what failed and why
if param_type is None:
    raise CompileError(
        f"Missing type annotation for parameter `{name}` in "
        f"function [hot_pink]{func.__name__}[/hot_pink]."
    )
  • Catch specific errors when it makes sense to do so.
  • When propagating errors from an exception handler, chain exceptions with raise ... from ex to preserve context.
try:
    result = risky_operation()
except SpecificError as ex:
    logger.error(f"Operation failed: {ex}")
    raise CompileError(f"Failed to process: {ex}") from ex
except Exception as ex:
    logger.exception("Unexpected error")
    raise

Testing

Tests follow the Arrange-Act-Assert pattern with comments, and are organized in a test/ directory that mirrors the source code structure.
  • Use descriptive test function names with test_ prefix
  • Group related tests in classes
  • Use clear assertions
def test_add_two_numbers():
    # Arrange
    num_a = 9
    num_b = 10
    # Act
    result = num_a + num_b
    # Assert
    assert result == 21
  • Test files should end with _test.py
  • Place tests in a test/ directory
  • Mirror the source code structure in test organization

Dependencies

We prefer well-maintained packages with pinned versions for critical dependencies, use optional import patterns with try/except blocks for non-critical features, and favor relative imports for internal modules.
  • Prefer well-maintained, widely-used packages
  • Pin versions for critical dependencies
  • Use optional imports for non-critical features
# Optional import pattern
try:
    from onnxruntime import InferenceSession
except ImportError:
    InferenceSession = None

def onnx_model_inference(model):
    if InferenceSession and isinstance(model, InferenceSession):
        # Handle ONNX model
        pass
  • Use relative imports for internal modules
  • Keep dependencies between modules minimal
  • Avoid circular imports