Code Style Guide
RevitPy enforces style through ruff (linting and formatting) and mypy (type checking). The authoritative configuration lives in pyproject.toml. This page summarises the rules and supplements them with conventions observed in the codebase.
Ruff Configuration
General Settings
[tool.ruff]
target-version = "py311"
line-length = 88
extend-exclude = ["cli/templates/"]
- Target Python version is 3.11, so modern syntax (
X | Yunions,matchstatements, etc.) is acceptable. - Maximum line length is 88 characters (the
ruff formatdefault, matching Black). - The
cli/templates/directory is excluded from linting.
Enabled Rule Sets
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort (import sorting)
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"S", # flake8-bandit (security)
]
| Code | Rule set | What it checks |
|---|---|---|
E |
pycodestyle | Whitespace, indentation, syntax style |
W |
pycodestyle | Style warnings |
F |
pyflakes | Unused imports, undefined names, etc. |
I |
isort | Import ordering and grouping |
B |
flake8-bugbear | Common Python pitfalls |
C4 |
flake8-comprehensions | Unnecessary list/dict/set comprehension patterns |
UP |
pyupgrade | Python version upgrade opportunities |
S |
flake8-bandit | Security-related issues |
Ignored Rules
The following rules are currently suppressed project-wide:
| Code | Reason |
|---|---|
E501 |
Line length handled by ruff format |
B008 |
Function calls in argument defaults (used intentionally) |
C901 |
Function complexity (not enforced) |
B904 |
raise ... from within except (TODO: fix) |
E722 |
Bare except (TODO: fix) |
E741 |
Ambiguous variable names |
UP007 |
X \| Y union syntax for type annotations |
B007 |
Unused loop variable |
B023 |
Function not binding loop variable |
B017 |
pytest.raises(Exception) |
F811 |
Redefinition of unused name |
S110 |
try-except-pass |
S112 |
try-except-continue |
S603 |
Subprocess call without shell=True check |
S607 |
Partial executable path in subprocess |
N802 |
Function name should be lowercase |
N805 |
First argument should be self |
Rules marked “TODO” indicate known technical debt.
Per-File Ignores
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # Unused imports (re-exports)
"tests/**/*" = ["S101", "D", "F401", "E402"] # assert, docstrings, imports
"**/tests/**/*" = ["S101", "S105", "S106", "D", "F401", "E402"]
"bridge/**/*" = ["F401", "E402"]
"revitpy/performance/**/*" = ["F401"]
"cli/**/*" = ["F401", "E402", "B904"]
"revitpy-package-manager/**/*" = ["F401", "E402", "F403", "F405", "S"]
"proof-of-concepts/**/*" = ["S311"]
Notable: S101 (assert usage) is allowed in tests but flagged elsewhere.
Import Ordering
The I (isort) rule set is enabled. Ruff sorts imports into the standard groups:
- Standard library
- Third-party packages
- Local (first-party) imports
Within each group, imports are sorted alphabetically. Observed codebase pattern:
from __future__ import annotations
import asyncio
import threading
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Any, Generic, Protocol, TypeVar
from loguru import logger
from pydantic import BaseModel, validator
from .element import Element, ElementSet
from .exceptions import RevitAPIError, TransactionError
from __future__ import annotations is used consistently as the first import in modules that need it.
mypy Configuration
[tool.mypy]
python_version = "3.11"
ignore_missing_imports = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_no_return = true
strict_equality = true
show_error_codes = true
Active Strictness Flags
| Flag | Effect |
|---|---|
check_untyped_defs |
Type-check function bodies even without annotations |
no_implicit_optional |
def f(x: str = None) is an error; use x: str \| None = None |
warn_redundant_casts |
Warn on unnecessary cast() calls |
warn_no_return |
Warn when a function might not return |
strict_equality |
Flag comparisons between incompatible types |
show_error_codes |
Display error codes in output for targeted suppression |
Disabled Error Codes
The following error codes are currently disabled to allow incremental adoption. The comment in pyproject.toml says: “Relax rules with many pre-existing violations; tighten incrementally.”
attr-defined, assignment, var-annotated, valid-type, operator,
arg-type, misc, union-attr, override, return-value, index,
call-overload, no-redef, exit-return, has-type, dict-item,
truthy-function, type-var, type-abstract, import-untyped
New code should aim to pass with these checks enabled where practical. Avoid introducing new violations even if they would currently be suppressed.
Type Annotation Conventions
Patterns observed throughout the codebase:
from __future__ import annotationsis used in most modules to enable PEP 604 union syntax (X | Y) in all Python 3.11+ contexts.-
TypeVaris used for generic classes. Example fromrevitpy/api/query.py:T = TypeVar("T", bound=Element) -
Protocols define interfaces. The codebase uses
typing.Protocolextensively:class IRevitApplication(Protocol): @property def ActiveDocument(self) -> Any: ... class IElementProvider(Protocol): def get_all_elements(self) -> list[Element]: ... -
@runtime_checkableis used onIElementProviderto allowisinstancechecks. -
Return types are annotated on all public methods.
Nonereturn is written explicitly:def disconnect(self) -> None: -
Optional parameters use the
X | Nonesyntax (notOptional[X]):def __init__(self, revit_application: IRevitApplication | None = None) -> None:
Naming Conventions
Patterns observed in the codebase:
| Kind | Convention | Examples |
|---|---|---|
| Classes | PascalCase | RevitAPI, ElementSet, QueryBuilder, TransactionGroup |
| Protocols / Interfaces | I prefix + PascalCase |
IRevitApplication, IRevitDocument, IElementProvider, ITransactionProvider |
| Enums | PascalCase class, UPPER_SNAKE values | TransactionStatus.NOT_STARTED, FilterOperator.EQUALS |
| Functions / methods | snake_case | get_parameter_value, start_transaction |
| Private methods | underscore prefix | _convert_from_revit, _ensure_evaluated |
| Constants | UPPER_SNAKE_CASE | PERFORMANCE_THRESHOLDS, REVIT_VERSIONS |
| Type variables | Single uppercase letter or short name | T, R, E, P |
| Module-level “constants” | prefixed underscore + UPPER_SNAKE | _LAZY_EVAL_THRESHOLD, _KNOWN_FAILURES |
| Dataclasses | PascalCase | DocumentInfo, TransactionOptions, FilterCriteria |
Note: N802 (function name should be lowercase) and N805 (first argument should be self) are currently ignored. Some mock classes use PascalCase method names (e.g., GetParameterValue, SetParameterValue) to match the Revit API surface.
Docstring Conventions
- Modules have a top-level docstring explaining the module’s purpose.
-
Classes have a docstring describing their role. Example:
class RevitAPI: """ Main RevitPy API class providing high-level interface to Revit. """ -
Public methods have docstrings with
Args:,Returns:, andRaises:sections where appropriate. Example fromElement.get_parameter_value:def get_parameter_value(self, parameter_name: str, use_cache: bool = True) -> Any: """ Get parameter value with caching and type conversion. Args: parameter_name: Name of the parameter use_cache: Whether to use cached values Returns: Parameter value with appropriate Python type Raises: ElementNotFoundError: If parameter doesn't exist """ - The
D(pydocstyle) rule set is not in theselectlist, so docstring format is not enforced by ruff. The patterns above are conventions, not hard requirements.
Context Managers
Both Transaction and TransactionGroup implement __enter__ / __exit__ and __aenter__ / __aexit__, supporting both sync and async usage:
with api.transaction("Update walls") as txn:
...
async with api.transaction("Async update") as txn:
...
RevitContext in the ORM layer is also a context manager that calls dispose() on exit.
Error Handling
All custom exceptions inherit from RevitAPIError, which accepts a message and an optional cause (the underlying exception). Subclasses add domain-specific fields:
class TransactionError(RevitAPIError):
def __init__(self, message, transaction_name=None, cause=None):
...
class ElementNotFoundError(RevitAPIError):
def __init__(self, element_id=None, element_type=None, cause=None):
...
Use raise ... from e when re-raising (note: B904 is currently ignored but this is marked as technical debt to fix).
Logging
The codebase uses loguru (from loguru import logger) consistently. Log levels observed:
logger.debug(...)– internal operations (transaction start/commit, cache hits, query execution).logger.info(...)– significant operations (document opened, changes saved).logger.warning(...)– recoverable issues (parameter read failure, commit handler failure).logger.error(...)– operation failures that will raise an exception.