-
Notifications
You must be signed in to change notification settings - Fork 20
Add Python standards #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add Python standards #100
Conversation
|
This will need adding to the front page - not sure if it fits under the coding standards section or whether it should have its own one? |
|
|
||
| This document defines the internal development standards for building backend services in Python ([FastAPI](https://fastapi.tiangolo.com/tutorial/)) within Defra. It adheres strictly to the [UK Government Digital Service (GDS) Python style guide](https://gds-way.digital.cabinet-office.gov.uk/manuals/programming-languages/python/python.html) and [PEP 8](https://peps.python.org/pep-0008/), ensuring code is clear, consistent, and maintainable across all AI services (using tools like [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/), etc.). All engineers must follow these conventions when writing Python code for backend services. | ||
|
|
||
| **Note:** Python should **ONLY** be used for creating backend services related to AI or data. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like everyone is using data - maybe this needs to say "data science" or something like that?
|
|
||
| **Note:** Python should **ONLY** be used for creating backend services related to AI or data. | ||
|
|
||
| **Note:** Python should **NOT** be used for frontend development. The GDS Service Manual has information on using client-side JavaScript. For server-side JavaScript we use Node.js. The principles of [Progressive Enhancement](https://www.gov.uk/service-manual/technology/using-progressive-enhancement) should be used for the design and build of frontend digital services. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with this point but do we need to go into details of client side JavaScript? Could we just say don't use it for frontend and instead use Node.js and reference our Node.js and JavaScript standards?
| **Note:** Python should **NOT** be used for frontend development. The GDS Service Manual has information on using client-side JavaScript. For server-side JavaScript we use Node.js. The principles of [Progressive Enhancement](https://www.gov.uk/service-manual/technology/using-progressive-enhancement) should be used for the design and build of frontend digital services. | ||
|
|
||
| ## Function Annotations and Typing Conventions | ||
| - Use Type Hints for All Functions: All function and method definitions should include Python type annotations for parameters and return values (PEP 484 style). This improves readability and helps static analysis. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we should include them by default? We wouldn't say that JSDoc should be used for all JavaScript as it may be an unnecessary overhead so why would we be inconsistent with Python?
Is there a data science-y/AI-y reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh - have you tried reading Python code?! 🤣
|
|
||
| _In this example, `item_id` is an int, `verbose` is a bool with default False, and the function returns a dictionary mapping strings to any type._ | ||
|
|
||
| - PEP 484 Syntax: Follow standard typing syntax for annotations: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a linter that could enforce these conventions we could just mandate usage of? Similar to our StandardJS approach?
| # Standard library imports | ||
| import json | ||
| import os | ||
| from pathlib import Path | ||
|
|
||
| # Third-party imports | ||
| import requests | ||
| from fastapi import APIRouter, HTTPException | ||
|
|
||
| # Local application imports | ||
| from app import config | ||
| from app.models import Item | ||
| from app.utils import calculate_score |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pleasing to read for me as this is how I order module imports in Node.js. However, that is not a consistent approach taken across Defra and it's not something we mandate in the equivalent standards.
So I'm conflicted between whether this is a good idea because as a new language we've got more of an opportunity to be more opinionated from the beginning on lower level code choices, or if this is just going too low and will just lead to friction and debate for a hill I've got no passion to die on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this does feel a bit like stepping over the bounds for a standard, but then the Python ecosystem is a bit different to that for Node (where everything is equal) and .NET (where everything by Microsoft is equal).
There's Python modules that are built in C and are a rock-solid underpinning of things like data science, which are maybe a bit different to some package that a hobbyist knocked together at the weekend.
But I'm not a Python expert, so perhaps I'm not aware of some specific value in mandating this?
|
|
||
| - Catch Specific Exceptions: When catching exceptions, catch the specific exception types you expect. Avoid bare `except`: clauses, as they catch system-exiting exceptions (`SystemExit`, `KeyboardInterrupt`) and obscure the cause of errors. If you truly need to catch any exception, use `except Exception:` (which excludes `SystemExit` and `KeyboardInterrupt`). Even then, consider logging the exception and re-raising if not handling it meaningfully. | ||
|
|
||
| -Provide Informative Messages: When raising exceptions, include a clear message. For example: `raise ValueError("Threshold must be non-negative")` is more informative than just `raise ValueError`. In a FastAPI context, raising an `HTTPException(status_code, detail="...")` with a helpful message will return that info to API clients. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| -Provide Informative Messages: When raising exceptions, include a clear message. For example: `raise ValueError("Threshold must be non-negative")` is more informative than just `raise ValueError`. In a FastAPI context, raising an `HTTPException(status_code, detail="...")` with a helpful message will return that info to API clients. | |
| - Provide Informative Messages: When raising exceptions, include a clear message. For example: `raise ValueError("Threshold must be non-negative")` is more informative than just `raise ValueError`. In a FastAPI context, raising an `HTTPException(status_code, detail="...")` with a helpful message will return that info to API clients. |
|
|
||
| - No Magic Numbers/Strings: Avoid using unexplained literal numbers or strings in code. If a value has significance, give it a name as a constant. For instance, instead of using `if len(data) > 1024: ...`, define `MAX_PAYLOAD_SIZE = 1024` and use `if len(data) > MAX_PAYLOAD_SIZE:`. This makes the code self-documenting. | ||
|
|
||
| - Immutability: Treat constants as read-only. Do not modify constant values at runtime. If a configuration needs to change, it should be done via changing an environment variable or config file, not by reassigning an imported constant in code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can modify constants in Python? Urgh...
| ``` | ||
| In a FastAPI app, a common practice is to use Pydantic settings (BaseSettings) to define config classes that read from env variables. Either approach is fine, as long as sensitive info and environment-specific details are not hard-coded. | ||
|
|
||
| - No Magic Numbers/Strings: Avoid using unexplained literal numbers or strings in code. If a value has significance, give it a name as a constant. For instance, instead of using `if len(data) > 1024: ...`, define `MAX_PAYLOAD_SIZE = 1024` and use `if len(data) > MAX_PAYLOAD_SIZE:`. This makes the code self-documenting. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be more general standards that Python specific and should apply across all languages.
Do we have a Python Sonar profile to enforce these?
|
My overall comment would be this is very opinionated and I'm conflicted about that. As a new language we've got more opportunity to be so than we did with JavaScript and C# as teams brought 10 years of diverse history with them before we attempted to standardise. Setting clear expectations from the beginning and giving teams less to debate over are never a bad thing. But do worry if we're too opinionated on repo structures, import orders etc, then we'll encounter resistance and friction when we try to scale across teams and what's really important to us with Python may become lost in the detail. Appreciate this comment doesn't really help, but thems me thoughts. 😂 |
No description provided.