Files
TheRock/rocm-systems/projects/rocprofiler-compute/PYTHON_CODING_STYLE.md
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

25 KiB

Python Coding Style Guidelines

This document outlines coding conventions and best practices for Python development in the ROCprofiler-compute project.

Table of Contents

Function Length

Focus on what the function does, not arbitrary line counts. The right measure is whether the function has a single, clear purpose — not whether it exceeds a particular number of lines.

Break code into smaller functions to improve readability and reusability.

Rules

  • Optimize for clarity of purpose, not line count.
  • Split a function as soon as it has more than one responsibility, regardless of length.

Example

Good: Each function has a focused responsibility

def load_data(filepath: str) -> pd.DataFrame:
    """Load raw data from a CSV file."""
    return pd.read_csv(filepath)

def filter_valid_rows(df: pd.DataFrame) -> pd.DataFrame:
    """Remove rows that fail validation checks."""
    return df[df["status"] == "valid"]

Bad: One function doing too much

def load_and_process_data(filepath: str, output_path: str) -> None:
    """Load data AND validate it AND transform it AND save it."""
    df = pd.read_csv(filepath)
    # 15 lines of validation
    # 20 lines of transformation
    # 10 lines of output formatting
    df.to_csv(output_path)
    # Result: 50+ line function that is hard to test or reuse

Naming Conventions

Prefer descriptive names over brevity. A few extra characters in a name can save significant time when reading and understanding code.

Rule of thumb:

  • The smaller the scope of a function, the longer and more specific its name should be — a private helper called only once can afford to be very explicit about what it does.
  • The larger the scope of a variable, the longer and more specific its name should be — a variable visible across many lines or modules should leave no ambiguity about its purpose.

Rules

  • Use descriptive names; never shorten for brevity.
  • The smaller the scope of a function, the more specific its name must be.
  • The larger the scope of a variable, the more specific its name must be.
  • Do not use single-letter names, abbreviations, or vague names (data, info, result, tmp, val) except within a very tight, obvious loop.

Example 1: Concise Helper Functions

Good: Functions are focused and concise

def load_sys_info(filepath: str) -> pd.DataFrame:
    """Load system running info from a CSV file."""
    return pd.read_csv(filepath)

Bad: Combining multiple responsibilities in one function

def load_and_process_sys_info(filepath: str, filter_cols: list[str]) -> pd.DataFrame:
    """Load, validate, filter, and transform sys info."""
    df = pd.read_csv(filepath)
    df = df[filter_cols]
    # 10 lines of validation
    # 20 lines of transformation
    # Result: 35+ line function doing too much
    return df

Example 2: Breaking Down Complex Operations

Good: Extract calculation into separate function

def version_to_numeric(version_parts: list[int], max_len: int) -> int:
    """Convert version tuple to numeric value using base-1000 positional system."""
    version_numeric = 0
    for i, part in enumerate(version_parts):
        version_numeric += part * (1000 ** (max_len - i - 1))
    return version_numeric

Bad: Inline complex logic without extraction

def resolve_library_path(library_path: Optional[str]) -> Optional[str]:
    if not library_path:
        return library_path

    # ... 50+ lines of logic including version parsing, comparison,
    # and selection all in one function.
    # This makes the function hard to test and understand.

I/O and Computation Separation

I/O — reading files, sockets, environment variables, or command-line arguments — depends on external state and is hard to test in isolation. Computation transforms inputs into outputs deterministically. Mixing the two in one function makes the computation impossible to reuse without paying the I/O cost, and impossible to verify without staging external state.

Write pure computation functions that operate on in-memory data, and wrap them with thin I/O functions that load inputs and persist outputs.

Rules

  • Never mix I/O with computation in the same function.
  • Pure computation functions must accept data, not file paths or socket handles.
  • Confine I/O to thin wrappers that load/save data and delegate to the pure function.

Example

Good: Pure computation, isolated I/O

def compute_md5(data: bytes) -> str:
    """Pure computation — testable with any byte payload."""
    return hashlib.md5(data).hexdigest()

def hash_file(filepath: Path) -> str:
    """Thin I/O wrapper around the pure computation."""
    return compute_md5(filepath.read_bytes())

Bad: Computation tangled with I/O

def hash_file(filepath: Path) -> str:
    """Cannot test the hashing without producing a file on disk."""
    md5 = hashlib.md5()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            md5.update(chunk)
    return md5.hexdigest()

File I/O Encoding

Text-mode file I/O should be deterministic across machines. Bare open() picks an encoding from the runtime locale, which varies between systems and produces silent corruption or hard decode errors when a file's bytes don't match. Declare the encoding at every call site so the behavior is the same everywhere.

Rules

  • Always pass encoding="utf-8" to open() for text-mode reads and writes.
  • Keep committed configuration files (YAML, JSON, INI) ASCII-only. Use plain ASCII substitutes for typographic glyphs: x or * for multiplication, straight quotes for smart quotes, and -- for em dashes.

Example

Good: Encoding declared explicitly

with open(config_path, encoding="utf-8") as f:
    data = yaml.safe_load(f)

Bad: Encoding inherited from the runtime locale

with open(config_path) as f:
    data = yaml.safe_load(f)

Nested Functions

Defining a function inside another function (a closure or nested def) hides it from the rest of the module, prevents reuse, and makes the outer function harder to read. Reach for a module-level private helper (prefixed _) instead in almost every case.

Rules

  • Do not produce nested functions unless the inner function is genuinely private to the outer scope and not reusable elsewhere.
  • Always write module-level private helpers (prefixed _) over nested functions.

Example

Good: Module-level private helper

def _format_kernel_name(name: str, max_length: int) -> str:
    if len(name) <= max_length:
        return name
    return name[: max_length - 3] + "..."

def render_kernel_table(kernels: list[Kernel], max_length: int) -> str:
    rows = [_format_kernel_name(k.name, max_length) for k in kernels]
    return "\n".join(rows)

Bad: Nested function with no real reason to be nested

def render_kernel_table(kernels: list[Kernel], max_length: int) -> str:
    def format_name(name: str) -> str:
        if len(name) <= max_length:
            return name
        return name[: max_length - 3] + "..."

    rows = [format_name(k.name) for k in kernels]
    return "\n".join(rows)

When to Use Helper Functions

The main reasons to break code into smaller functions are:

  • Make code more readable by defining function signatures with clear inputs and outputs, making it easier to understand high-level logic without reading each function's implementation.
  • Simplify code reuse (open-closed principle) — new logic can be written by reusing existing functions in different combinations with minimal or no changes to existing code. Fewer changes to existing functions means a lower probability of regressions.

Always consider breaking code into smaller functions even if the code is used only once.

Extract helper functions when you encounter:

Rules

  • Extract a helper when logic is repeated, an expression benefits from a name, or a block mixes abstraction levels.
  • Search the codebase for an existing helper before writing a new one — reuse rather than duplicate.
  • Always combine existing helpers in new ways over modifying them (open-closed principle).

1. Repeated Code Patterns

Good: Extract repeated logic into a reusable function

def parse_formula_counters(formula: str) -> tuple[bool, list[str]]:
    """Extract counter names from a formula string."""
    if not isinstance(formula, str):
        return False, []
    visited = False
    counters = []
    # ... parsing logic
    return visited, counters

# Reused in multiple places
for formula in formulas:
    visited, counters = parse_formula_counters(formula)

Bad: Copy-pasting the same logic

# In function A:
if not isinstance(formula, str):
    return False, []
tree = ast.parse(formula)
# ... 20 lines of parsing

# In function B:
if not isinstance(formula, str):  # Duplicated!
    return False, []
tree = ast.parse(formula)  # Duplicated!
# ... same 20 lines of parsing

2. Complex Expressions That Need Clarification

Good: Name the operation

def to_max(*args: Any) -> Union[float, np.ndarray]:
    """Return the maximum value across scalar or Series arguments."""
    if len(args) == 1 and isinstance(args[0], pd.Series):
        return args[0].max()
    elif len(args) == 2 and (
        isinstance(args[0], pd.Series) or isinstance(args[1], pd.Series)
    ):
        return np.maximum(args[0], args[1])
    elif max(args) is None:
        return np.nan
    else:
        return max(args)

Bad: Inline complex conditional with unclear purpose

# What does this do? Hard to understand at a glance
result = (args[0].max() if len(args) == 1 and isinstance(args[0], pd.Series)
          else np.maximum(args[0], args[1]) if len(args) == 2
          and (isinstance(args[0], pd.Series) or isinstance(args[1], pd.Series))
          else np.nan if max(args) is None else max(args))

3. Improving Testability

See I/O and Computation Separation for the canonical pattern: keep computation pure and confine I/O to thin wrappers so the computation can be exercised with in-memory inputs.

Good: Pure computation is trivially testable

def compute_md5(data: bytes) -> str:
    """Pure computation — testable with any byte payload."""
    return hashlib.md5(data).hexdigest()

def hash_file(filepath: Path) -> str:
    """Thin I/O wrapper around the pure computation."""
    return compute_md5(filepath.read_bytes())

Bad: Bundling unrelated work makes everything hard to test

def process_and_hash_file(filepath: Path) -> tuple:
    """Does too much — hash, validate, and transform are all entangled."""
    data = filepath.read_bytes()
    return (
        hashlib.md5(data).hexdigest(),
        validate_data(data),
        transform_data(data),
    )

When NOT to Extract Helper Functions

Not every block deserves to become a function. Extracting when there's no clarity gain just adds indirection a reader has to chase.

Rules

  • Don't extract when the helper's name adds no information over reading the code directly.
  • Don't extract when the operation is a single standard-library call.
  • Don't write functions whose body is a single, already-readable expression.
  • Don't write functions whose name merely repeats what the code obviously does (e.g., def get_len(x): return len(x)).

Example

# Bad: Unnecessary helper — the name adds no clarity
def is_empty(lst: list) -> bool:
    return len(lst) == 0

# Good: Direct usage is equally readable
if len(my_list) == 0:
    pass
# Bad: Helper that just wraps an already-clear operation
def get_column_names(df: pd.DataFrame) -> list[str]:
    return list(df.columns)

# Good: Direct usage is just as clear
column_names = list(df.columns)

Single Responsibility Principle

Each function should do ONE thing well. If you use "and" to describe what it does, it likely needs splitting.

Rules

  • Every function must do exactly one thing.
  • If "and" appears in the function's description, split it.

Example 1: Clear Single Purpose

Good: Does one thing — creates a dataframe

def create_df_pmc(
    raw_data_dir: str,
    kernel_verbose: int,
    verbose: int,
    config_dict: dict[str, Any],
) -> pd.DataFrame:
    """Load all raw pmc counters and join into one dataframe."""
    # Single responsibility: load counters into a DataFrame and return it.
    df = pd.read_csv(Path(raw_data_dir) / "pmc_perf.csv")
    if config_dict.get("format_rocprof_output") == "rocpd":
        df = utils_analysis.process_rocpd_csv(df)
    kernel_name_shortener(df, kernel_verbose)
    return df

Bad: Multiple responsibilities

def load_validate_transform_and_save_data(
    raw_data_dir: str,
    output_path: str,
    validation_rules: dict,
    transform_config: dict,
) -> bool:
    """Load data AND validate it AND transform it AND save it."""
    df = pd.read_csv(raw_data_dir)

    if not validate(df, validation_rules):
        return False

    df = transform(df, transform_config)
    df.to_csv(output_path)
    return True

Example 2: Focused Data Processing

Good: Single purpose per function

def pre_processing(self) -> None:
    """Perform pre-processing steps prior to analysis."""
    super().pre_processing()
    args = self.get_args()

    for path_info in args.path:
        workload = self._runs[path_info[0]]

        # Each operation delegated to a focused helper
        workload.raw_pmc = file_io.create_df_pmc(...)

        if self._profiling_config.get("iteration_multiplexing") is not None:
            workload.raw_pmc = self.iteration_multiplex_impute_counters(
                workload.raw_pmc,
                policy=self._profiling_config["iteration_multiplexing"],
                workload_dir=Path(path_info[0]),
            )

        file_io.create_df_kernel_top_stats(...)
        kernel_name_shortener(workload.raw_pmc, args.kernel_verbose)
        parser.load_table_data(...)

Bad: Doing everything inline

def pre_processing(self) -> None:
    """Do everything in one place."""
    args = self.get_args()

    for path_info in args.path:
        workload = self._runs[path_info[0]]

        # 50 lines of inline DataFrame creation logic
        dfs = []
        for csv_file in Path(path_info[0]).rglob("*.csv"):
            # ... lots of processing

        # 30 lines of inline imputation logic
        if self._profiling_config.get("iteration_multiplexing"):
            # ... complex counter imputation

        # 40 lines of inline stats creation
        # ... more processing

        # Result: 150+ line function that is hard to test or understand

Levels of Abstraction

An abstraction level refers to how much detail a piece of code exposes. High-level code describes what to do (e.g., setup_configuration(), run_analysis()), while low-level code describes how to do it (e.g., opening files, manipulating strings, iterating bytes). Mixing these within a single function makes the overall flow harder to follow.

Keep functions at one abstraction level. Don't mix high-level operations with low-level details.

Rules

  • Every statement in a function must operate at the same abstraction level.
  • High-level orchestration functions call named helpers — they do not contain raw file I/O, string manipulation, or byte operations inline.
  • Before adding a low-level operation to a high-level function, extract it into a helper.

Example 1: Consistent Abstraction Level

Good: High-level coordination

def run_profiler(
    config_files: Union[list[str], str],
    profiler_options: dict,
    workload_dir: str,
    # ...
) -> None:
    """High-level profiling orchestration."""
    # All operations at the same abstraction level
    setup_counter_definitions(config_files, workload_dir)

    if is_live_attach_mode:
        perform_attach_detach(env, options)
    else:
        success, output = capture_subprocess_output(app_cmd, env)

    process_profiling_results(workload_dir)

Bad: Mixing abstraction levels

def run_profiler(config_files, profiler_options, workload_dir) -> None:
    """Mixes high and low level operations."""
    # High-level: setup counter definitions
    setup_counter_definitions(config_files, workload_dir)

    # Suddenly drops to low-level details inline
    if options.get("attach_pid") is not None:
        with open("/tmp/config.yaml", "w") as f:
            yaml.dump(counter_defs, f)
        env["METRICS_PATH"] = str(
            Path(tempfile.mkdtemp(prefix="profiler_", dir="/tmp"))
        )

Example 2: Delegation to Appropriate Level

Good: Delegate low-level details to libraries

def convert_counter_format(
    counter_file: str,
    agent_info_filepath: str,
    output_file: str,
) -> None:
    """Convert counter file from v3 to v2 format."""
    counters = pd.read_csv(counter_file)
    agent_info = pd.read_csv(agent_info_filepath)

    # pandas handles the low-level details
    result = counters.pivot_table(...)
    result = result.merge(agent_info[...], ...)
    result.rename(columns=name_mapping, inplace=True)
    result.to_csv(output_file, index=False)

Bad: Implementing low-level logic when libraries exist

def convert_counter_format(counter_file: str, output_file: str) -> None:
    """Don't reinvent the wheel."""
    rows = []
    with open(counter_file) as f:
        for line in f:
            fields = []
            current = ""
            in_quotes = False
            for char in line:
                if char == '"':
                    in_quotes = not in_quotes
                elif char == "," and not in_quotes:
                    fields.append(current)
                    current = ""
                # ... 50 more lines of manual CSV parsing

Avoiding Deep Nesting

Deep indentation forces a reader to hold many active conditions in their head at once. Keep the main path of a function at the leftmost level and push edge cases to the top.

Rules

  • Maximum 2 levels preferred; 3 levels is the hard limit.
  • Use guard clauses (early returns/continues) to eliminate nesting rather than adding else branches.
  • Invert conditions when possible — prefer if not condition: return over wrapping the body in if condition: { ... }.
  • If reaching the nesting limit, extract the inner block into a named function.

Example 1: Early Returns

Good: Guard clauses reduce nesting

def resolve_library_path(library_path: Optional[str]) -> Optional[str]:
    """Use early returns to avoid deep nesting."""
    if not library_path:
        return library_path

    path = Path(library_path)

    if path.exists():
        return str(path)

    # Continue with main logic at a low nesting level
    matches = glob.glob(f"{glob.escape(library_path)}.*")
    version_tuples: list[tuple[list[int], str]] = []

    for candidate in matches:
        if not candidate.startswith(library_path):
            continue  # Early continue
        # Process at shallow nesting

Bad: Deep nesting

def resolve_library_path(library_path: Optional[str]) -> Optional[str]:
    """Avoid this deep nesting."""
    if library_path:  # Level 1
        path = Path(library_path)
        if path.exists():  # Level 2
            return str(path)
        else:  # Level 2
            matches = glob.glob(f"{glob.escape(library_path)}.*")
            if matches:  # Level 3 - already at limit
                for candidate in matches:  # Level 4 - exceeds recommendation
                    if candidate.startswith(library_path):  # Level 5
                        suffix = candidate[len(library_path):]
                        if suffix.startswith("."):  # Level 6
                            # ...

Example 2: Extract Nested Logic

Good: Extract complex nested logic into helpers

def create_filtered_stats(
    df_in: dict[str, pd.DataFrame],
    filter_nodes: Optional[list[str]],
    filter_gpu_ids: Optional[list[str]],
    filter_dispatch_ids: Optional[list[str]],
) -> None:
    """Main function delegates complex nested operations."""
    df = df_in["raw"].copy()

    if filter_nodes:
        df = df.loc[df["Node"].astype(str).isin(filter_nodes)]

    if filter_gpu_ids:
        df = df.loc[df["GPU_ID"].astype(str).isin(filter_gpu_ids)]

    if filter_dispatch_ids:
        df = apply_dispatch_filter(df, filter_dispatch_ids)

Bad: Everything nested inline (exceeds 2-3 level recommendation)

def create_filtered_stats(df_in, filter_nodes, ...) -> None:
    """Deep nesting makes this hard to follow."""
    df = df_in["raw"].copy()

    if filter_nodes:  # Level 1
        if isinstance(filter_nodes, list):  # Level 2
            for node in filter_nodes:  # Level 3 - at limit
                if node in df["Node"].values:  # Level 4 - exceeds recommendation
                    # ... filter logic
                    if validate_node(node):  # Level 5
                        # Too deep!
                        pass

Code Organization

Rules

  • Module structure order: docstring → imports (stdlib → third-party → local, sorted within each group) → constants → public functions → private helpers → classes.
  • Public functions appear before private helpers in every file, except when a private helper is used as a decorator on those public functions. Python evaluates decorators at module load, so the helper must precede the functions it decorates.
  • The _ prefix marks privacy for module-level helpers and class members only — do not use it for helpers in test files (test_*.py). Test modules are imported only by pytest for collection and should not import each other (use conftest.py for shared fixtures), so there is no internal API to mark private.
  • Use is None / is not None — never == None or != None.

None Comparisons

Always use is None and is not None — never == None or != None. None is a singleton and identity comparison is both correct and more explicit.

# Good
if value is None:
    return default

# Bad
if value == None:  # Caught by Ruff (E711)
    return default

File Structure

Organize Python modules with this structure. Putting public functions first lets a reader understand the module's API without wading through implementation detail.

# 1. Docstring
"""Module description."""

# 2. Imports (grouped and sorted)
import os
import sys

import pandas as pd
import yaml

from mypackage.logger import log
from mypackage.specs import MachineSpecs

# 3. Constants
MAX_SIZE = 100
DEFAULT_TIMEOUT = 30

# 4. Public functions
def public_api_function() -> None:
    """Public API exposed to other modules."""
    pass

# 5. Helper functions (private, prefixed with _)
def _internal_helper() -> str:
    """Private helper function."""
    pass

# 6. Classes
class DataProcessor:
    """Class for processing data."""
    pass

Example

Good: Well-organized module structure

#!/usr/bin/env python3
# License header...

"""
Hash manager for tracking configuration file changes.
Can be used standalone or imported by the master workflow.

Usage:
    python hash_manager.py --compute-all <configs_dir>
"""

from __future__ import annotations

import argparse
import hashlib
import json
import sys
from pathlib import Path
from typing import Optional

# Constants
DEFAULT_HASH_DB = "config/.hashes.json"

# Public functions in logical order
def compute_file_hash(filepath: Path) -> str:
    """Compute MD5 hash of a file."""
    # ...

def compute_arch_hashes(arch_dir: Path) -> dict:
    """Compute hashes for all YAML files in an arch directory."""
    # ...

def load_hash_db(hash_file: Path) -> dict:
    """Load hash database from file."""
    # ...

Bad: Disorganized structure

# No docstring
import sys
from mypackage.logger import log  # Mixed import groups
DEFAULT_HASH_DB = "config/.hashes.json"  # Constant placed before imports
import hashlib  # Import after constant
import argparse

# Function used before it's defined
def main():
    result = compute_hash()  # Defined later

# Private function mixed with public
def _helper():
    pass

def compute_hash():
    return _other_helper()  # Defined even later

def _other_helper():
    pass

Key Principles Summary

Principle Guideline
Readability first Code is read far more than written — optimize for clarity and maintainability over brevity
Single responsibility Each function should do exactly one thing well
Consistent abstraction Keep operations at the same conceptual level within a function
Shallow nesting Use guard clauses and early returns to keep nesting to 2-3 levels
Meaningful extraction Extract helpers when they improve clarity, reusability, or testability
Appropriate naming Prefer descriptive names; readability matters more than brevity