17 KiB
Coding Standards
Code Conventions Checklist
Checklists for writing and reviewing code.
General:
- Code does not use hard-coded "magic" strings.
- Code does no use any hard-coded "magic" numbers.
- All comments add value and follow our commenting guidelines.
- No urls appear as hard-coded path and all use Django url names and
reverse(). - There no .flake8 linting violations.
- The file ends with a newline
- All new files follow the project structure's name and location conventions.
- There are no Emoji's in any code, templates or documentation.
Imports:
- All module imports are at the top of the file.
- Imports are grouped logically: system/pip, django, project, apps then module local.
- Imports have one line space between groups.
- Within a group, imports are sorted alphabetically
- No module relies on indirect (hidden) imports
- There are no unused imports
Method Declarations:
- All methods uses type hints for their parameters and return values.
- All method definition with more than two arguments use one line per argunment.
- All multi-line method signatures have all their types and default values aligned.
- No methods return position-dependent tuples.
Class Declarations:
- All dataclass definitions have all their types and default values aligned.
- All enum definitions have all their types and default values aligned.
- Are enums subclass LabeledEnum
Method Calling:
- All method calls use named parameters.
- All metghod calls with more than two argumants use one line per argument.
- All multi-line method calls use a comma after last item.
- Spaces surrounding all equals ("=") signs when passing parameters to methods.
Expressions:
- All boolean assignments to conditional clauses are wrapped in
bool(). - All loops end with an explicit
continueor areturn. - All methods end with an explicit
returnor araise? - All multi-line arrays, sets, dictionaries use a comma after last item.
- Compound/complex conditional statements use explicit delimiting parentheses.
- Single quote are used for all strings in Python code.
- All multi-line arrays, dictionaries and sets use a comma after last item.
Views:
- All url paths components follow our standard ordering conventions.
- All url Django names follow our standard naming conventions
- All view names match url names (except for casing and underlining).
- ALl views raise exceptions for common error conditions. (Let middleware handle it.)
Templates:
- Template names referenced in views closely match the view names that use them.
- No templates have in-line Javascript.
- No templates have in-line CSS.
- Templates appear in a subdirectory matching their purpose: modals, panes, pages.
- Template tags
loadstatments near the top of the file.
Comments:
- No comments that state what is obvious from the naming and typing and context.
- No method docstrings that simply restate what the method name already says.
- No inline comments that describe what the code is doing (vs why).
- No comments that refer to the past, future or current work in progress.
Code Conventions Details
No "magic" strings
We do not use "magic" or hard-coded strings when needing multiple references. Any string that need to be used in two or more places is a risk of them being mismatched. This includes, but is not limited to:
All DOM ids and class strings that are shared between client and server must adhere to our DIVID pattern. See "Client-Server Namespace Sharing" in Front End Guidelines.
Type Hints
- We add type hints to dataclass fields, method parameters and method return values.
- We do not add type hints to locally declared method variables.
- Some allowed, but not required exceptions:
- The
requestparameter when appearing in a Django view class. - Single parameter methods where the method name or parameter name makes its type unambiguous.
- The
Method Parameter Formatting
For readability, besides adding type hints to method parameters, we adhere to the following formatting conventions:
- For methods with a single parameter, or parameters of native types, they can appear in one line with the method name.
- If more than one parameter and app-defined types, then use a multiple line declaration.
- For methods with three or more parameters, we use one line per parameter and align the type names.
Good Examples:
def set_entity( self, entity_id : int ) -> EntityPath:
def set_entity_order( self, entity_id : int, rank : int ) -> EntityPath:
def set_entity_path( self,
entity_id : int,
location : Location,
svg_path_str : str ) -> EntityPath:
Bad Examples:
def set_entity_type( self, entity_id : int, entity_type : EntityType ) -> EntityPath:
def set_entity_path( self,
entity_id : int,
location : Location,
svg_path_str: str ) -> EntityPath:
def set_entity_path( self, entity_id : int,
location : Location, svg_path_str: str ) -> EntityPath:
Variable Assignment vs Inlining
We prefer explicit variable assignment over inlining function calls. This is not about minimizing lines of code - it's about readability and debuggability.
Good - Named intermediate values
table_name = self.queryset.model._meta.db_table
logger.debug( f"Processing table: {table_name}" )
cutoff_date = datetimeproxy.now() - timedelta( days=30 )
old_records = queryset.filter( created__lt=cutoff_date )
Bad - Inlined function calls
logger.debug( f"Processing table: {self.queryset.model._meta.db_table}" )
old_records = queryset.filter(
created__lt=datetimeproxy.now() - timedelta( days=30 )
)
Benefits of variable assignment:
- Provides semantic naming that clarifies intent
- Easier to debug (can inspect intermediate values)
- Improves readability by breaking complex expressions
- Allows reuse without recalculation
Explicit Booleans
We prefer to wrap all expression that evaluate to a boolean in bool() to make it explicit what type we are expecting:
Good
my_variable = bool( len(my_list) > 4 )
Bad*
my_variable = len(my_list) == 4
Complex Boolean Expressions
- For boolean clauses and conditionals where there are multiple clauses, we prefer to explicitly enclose each clause with parentheses in order to make the intention clear.
- We do not rely on the user having a deep understanding of the compiler's ordeer of precedence.
- We use one line per clause unless the combined clauses are very short and obvious.
- Single boolean typed variables or methods that return a boolean do not need paretheses.
Good:
if is_editing and location_view:
pass
if (( hass_state.domain == HassApi.SWITCH_DOMAIN )
and ( HassApi.LIGHT_DOMAIN in prefixes_seen )):
pass
if ( HassApi.BINARY_SENSOR_DOMAIN in domain_set
and device_class_set.intersection( HassApi.OPEN_CLOSE_DEVICE_CLASS_SET )):
pass
Bad:
if hass_state.domain == HassApi.SWITCH_DOMAIN and HassApi.LIGHT_DOMAIN == 'foo':
pass
Control Flow Statements
- Always include explicit
continuestatements in loops - Always include explicit
returnstatements in functions - This improves code readability and makes control flow intentions explicit
Example:
def process_items(items):
results = []
for item in items:
if not item.valid:
continue # Explicit continue for invalid items
if item.needs_processing:
result = process(item)
results.append(result)
continue # Explicit continue after processing
# Handle non-processing case
results.append(item.default_value)
continue # Explicit continue at end of loop
return results # Explicit return at end of function
Operator Spacing
- Use spaces around assignment operators and most other operators in expressions
- Examples:
x = y + z,result += value,if count == 0 - Exception: Don't add spaces in function keyword arguments (
func(x=y)) or type annotations
Parentheses Spacing (Deliberate PEP8 Deviation)
- We prefer spaces inside parentheses for enhanced readability
- This is a deliberate deviation from PEP8 standards (E201, E202)
- Examples:
- Good:
if ( condition ): - Good:
my_function( param1, param2 ) - Good:
result = calculate( x + y )
- Good:
- This applies to all parentheses: function calls, conditionals, expressions
- Rationale: Extra spacing improves readability by visually separating content from delimiters
Boolean Expressions
When assigning or returning boolean values, wrap expressions in bool() to make intent explicit:
# Good - explicit boolean conversion
is_active = bool(user.last_login)
in_modal_context = bool(request.POST.get('context') == 'modal')
# Avoid - implicit boolean conversion
is_active = user.last_login
in_modal_context = request.POST.get('context') == 'modal'
Linting: Flake8 Configurations
The project uses two different flake8 configurations:
- Development Configuration (
src/.flake8) : Our preferred style for daily development work, with specific whitespace deviations from PEP8 for enhanced readability:- E201, E202: We use spaces inside parentheses for better visual separation
- E221: We align operators and values in multi-line declarations
- E251: We use spaces around keyword parameters for consistency
- Note: These are deliberate choices for improved code readability, not oversights
- CI Configuration (
src/.flake8-ci): GitHub Actions enforces these standards and blocks PR merging if violations exist.
Commenting Guidelines
- We avoid over-commenting and let the code variable/method naming be clear on the intent.
- We believe in the philosophy: "Before add a comment, ask yourself how the code can be changed to make this comment unnecessary."
- Do not add comments that are not timeless and refer to work in progress or future work. i.e., it must make sense for future readers of the code.
- Comments should explain the why not the what. Good comments document:
- Non-obvious design decisions
- Complex business logic
- External API/library quirks and workarounds
- Time-based complexities
- Bug fixes that prevent regression
- Avoid comments that:
-
State what the code obviously does
-
Contain development artifacts or work-stream context
-
Leave commented-out code (creates confusion)
-
Explain what better naming could clarify
-
When in doubt, ask: "Can I change the code to make this comment unnecessary?"
-
Special Cases
TRACE Pattern (Accepted)
TRACE = False # for debuggingis an accepted pattern- Addresses Python logging limitation (lacks TRACE level below DEBUG)
Examples
GOOD Comments - What TO Include
-
Design rationale that is non-obvious
- Example:
# Single source of truth for position vs path classification - Explains architectural decisions
- Example:
-
Complex domain logic explanations
- Example: Multi-line explanation of entity delegation concept
- Business rules that aren't obvious from code structure
-
Summarize complex or non-standard coding approaches
- When using unusual patterns or workarounds
- Algorithm explanations that aren't obvious
-
Design decision rationale
- Example: "The general types could be used for these, since all are just name-value pairs. However, by being more specific, we can provide more specific visual and processing"
- Explains why one approach was chosen over alternatives
-
Mathematical/geometric calculations
- Example:
'80.0,40.0', # top-left (100-20, 50-10) - Coordinate calculations that are difficult to verify mentally
- Especially valuable in test cases for validation
- Example:
-
Cross-file coordination notes
- Example:
# Match SvgItemFactory.NEW_PATH_RADIUS_PERCENT - Important synchronization between related constants/values in different files
- Example:
-
Complex domain abstractions
- Example: Multi-line explanation of LocationItem interface concept
- Abstract concepts that need implementation guidance
-
Multi-step process/algorithm documentation
- Example:
alert_manager.py:40-56- Breaks down the three distinct checks and explains why HTML is always returned - Complex workflows that need step-by-step explanation of the "why"
- Example:
-
External API/library limitations and workarounds
- Example:
wmo_units.py:838-841- Documents Pint library limitations requiring unit mappings - Example:
zoneminder/monitors.py:81-87- pyzm timezone parsing quirks - Critical for understanding why non-obvious code patterns exist
- Brief expressions of frustration (e.g., "ugh") acceptable when documenting known pain points
- Example:
-
External service configuration rationale
- Example:
usno.py:59-62- Documents API priority, rate limits, polling intervals - Explains constraints and decisions for external integrations
- Future extension points
- Example:
daily_weather_tracker.py:96-100- "Future: Add other weather field tracking here" - Marks logical insertion points for anticipated features
- Should be brief hints, not commented-out code blocks
- Temporal/timing complexity in APIs
- Example:
zoneminder/monitors.py:99-110- Events as intervals vs points, open/closed handling - Example:
zoneminder/monitors.py:166-171- Why polling time cannot advance - Critical for understanding time-based edge cases in external systems
- Bug fix documentation
- Example:
zoneminder/monitors.py:132-133- "This fixes the core bug where..." - Documents what was broken and why the current approach fixes it
- Helps prevent regression
BAD Comments - What NOT To Include
-
Method docstrings that restate the obvious
- Bad:
"""Get the total number of records in the history table."""for_get_record_count()in a HistoryTableManager class - Bad:
"""Delete records with the given IDs."""for_delete_records( ids ) - Bad:
"""Types of cleanup operation results."""for an enum called CleanupResultType - The method/class name + parameters + return type already convey this information
- Exception: Public API methods may need docstrings for documentation generation
- Bad:
-
Avoid commenting obvious variable purposes
- Bad:
# Store original entity_type_str to detect changes - Bad:
# Human-readable message for health statuswhen field is namedreason: str - The variable name and type should make this clear
- Bad:
-
Remove work-stream artifacts
- Bad: Comments explaining why tests were removed or referencing specific issues
- Comments should be timeless, not tied to particular development contexts
-
Redundant descriptions of clear code
- Bad:
# Track EntityType change and response needed after transaction - Bad:
# Check if we're over the total record limitbeforeif total_count <= self.max_records_limit: - When variable names and conditionals already convey this information
- Bad:
-
Cryptic references
- Bad:
# Recreate to preserve "max" to show new form - If unclear, either explain properly or remove
- Bad:
-
Development phase/work-stream artifacts
- Bad: Comments explaining "Phase 3/Phase 4" development contexts
- Bad: Explanations of why code was removed or changed
- These belong in commit messages or PR descriptions, not in main branch code
-
TODO comments (high bar to justify)
- Generally avoid TODOs in main branch
- If important enough for TODO, create an issue and prioritize it
- Only acceptable when there's a compelling reason not to address immediately
- Must be specific and actionable, not vague intentions
-
Commented-out code
- Bad: Dead code that's been disabled or replaced
- Bad: Example implementations as large code blocks
- Bad: Logic that looks like it might need uncommenting (e.g.,
zoneminder/integration.py:65-66) - If needed for future reference, create an issue instead
- Exception: Very brief one-line hints at extension points (see "Future extension points" in good comments)
- Commented code creates confusion about whether it should be active
Related Documentation
- Testing standards: Testing Guidelines
- Backend patterns: Backend Guidelines
- Frontend standards: Frontend Guidelines
- Workflow and commits: Workflow Guidelines