Skip to main content
Adopting a uniform return convention is most effective when it is applied consistently. A single function that raises instead of returning, or that skips msg on success, creates a gap that erodes the pattern’s value. The practices below help teams establish a codebase-wide standard that makes every function predictable — whether you are reading it for the first time or debugging it at 2 AM.
Declare response = DataAndMsgReturn() (or response = BasicReturn()) as the very first line of the function body, before any logic, guards, or branching. This ensures there is always a valid return value no matter how control flow exits the function, and it makes the intended return type immediately visible to anyone reading the code.
Avoid initializing the response inside an if branch or late in the function body — it makes it easy to accidentally return an uninitialized value on some code paths.
If a function performs an action but has no meaningful payload to return — writing a file, deleting a record, sending a notification — use BasicReturn rather than DataAndMsgReturn. This keeps the function signature honest: it tells callers up front that there is no data to inspect, only a success or failure signal.
Returning a DataAndMsgReturn with an empty data field from a write-only function is not wrong, but it misleads readers into expecting a payload that never arrives. Match the class to the intent.
The msg field is not only for error messages. Setting a clear, descriptive message on the success path — "User created successfully", "Config loaded and validated" — makes logs and debug output meaningful without any extra tooling. A function that only sets msg on failure gives you half the picture.
When msg is consistently populated, str(result) and structured logging give you useful audit trails at no extra cost.
A function that declares -> DataAndMsgReturn or -> BasicReturn as its return type has made a contract: it will always return that type, and it will never raise. Wrap the entire function body in try/except Exception and set ok = False if anything goes wrong.
If a caller receives a DataAndMsgReturn, they should never need a try/except around the call — they rely on result.ok instead.
When chaining operations, always forward the original exception object from the inner result to the outer one. Do not replace result.error with a plain string, and do not discard it by creating a fresh exception. The original exception carries the full traceback, the original message, and any metadata attached by the inner function.
Swallowing the original error makes post-mortem debugging significantly harder, especially across service boundaries or in production logs.
Call to_dict() only at the outermost layer of your application — an API route handler, a CLI output formatter, a message queue publisher. Deep inside business logic, pass DataAndMsgReturn or BasicReturn objects directly, keeping the Pydantic model intact and all fields accessible as attributes.
Serializing early loses attribute access (result.data, result.msg) and prevents chaining — keep the model as a model until you need a dictionary.
Always declare -> DataAndMsgReturn or -> BasicReturn in your function signatures. Type annotations make the intended contract visible at a glance, enable IDE auto-completion for .ok, .data, .error, and .msg, and allow MyPy to catch mismatches before they reach production.
In a team setting, annotated return types act as living documentation — they communicate intent to every developer who reads or calls the function.
BasicReturns is fully compatible with MyPy. Because BasicReturn and DataAndMsgReturn are Pydantic BaseModel subclasses with typed fields, annotating your functions and running mypy will catch type mismatches — such as returning a raw dict from a -> DataAndMsgReturn function, or accessing .data on a BasicReturn — before they reach your test suite or production environment.