Defensive programming
Reading time15 minIn brief
Article summary
In this course, we introduce defensive programming, which is a way of writing your programs that anticipates possible user errors. The objective is to reduce the number of possible crashes due to bad usage, and to provide useful error messages to the user.
Let is be clear already, you will never be able to imagine all the possible errors a user will make! However, trying to do it will help the user understand common problems and adapt their behavior accordingly.
“A common mistake that people make when trying to design something completely foolproof is to underestimate the ingenuity of complete fools.” — Douglas Adams, Mostly Harmless
Main takeaways
-
Defensive programming is about making your code more resilient to unexpected inputs and conditions.
-
It consists in two main problems: identifying problem situations and identifying corrective actions.
-
It involves validating inputs, avoiding assumptions, using safe defaults, and handling errors gracefully.
Article contents
1 — Principles of defensive programming
1.1 — What is it?
Defensive programming makes it possible to develop programs that are able to detect anomalies and make pre-determined responses by assuming that errors can occur and protecting against them. It ensures that a piece of software will continue to function under unforeseen circumstances.
Defensive programming practices are often used where high availability, safety or security is required.
It may also be very useful for educational purpose.
Think of PyRat for instance, which is full of defensive programming assertions.
You are probably more happy with a message that says “Invalid direction received in the turn
function” than “The game has crashed”.
Defensive programming is an approach to improving software and source code in terms of:
- Overall quality – Reducing the number of bugs and problems in the software.
- Making the source code understandable – So that it can be accepted in a code audit.
- Making the software behave in a predictable way – Despite unexpected inputs or user actions.
However, overly defensive programming can protect against bugs that will never occur, resulting in running and maintenance costs. It is important to find a balance between too much and not enough.
Let’s make a parallel with driving carefully. Defensive drivers assume that other drivers will make mistakes and take steps to protect themselves. Defensive drivers anticipate dangerous situations and adjust their driving habits to avoid accidents.
As you write your code, you make many assumptions about the state of the program and the data it processes. For example:
- A variable’s value is always within a certain range.
- A file exists and can be read.
- A network connection is always available.
The correctness of your code depends on these assumptions being true. However, in the real world, things can go wrong, e.g.:
- A user enters invalid data.
- A file is missing.
- A network connection is lost.
Faulty assumptions can lead to runtime errors, which can cause your program to crash or produce incorrect results.
1.2 — Defensive vs. offensive programming
Defensive programming is a way of writing your programs that anticipates possible user errors. Thus, it is the opposite of offensive programming. Offensive programming is based on the idea that it is the responsibility of those who use a service to verify the conditions of use of this service.
Defensive programming consists of two problems:
- Identifying problem situations (division by zero, root of a negative number, etc.) for all or part of software application.
- Identifying corrective actions to be implemented by systematically avoiding to escalate the problem to calling services.
2 — Key concepts of defensive programming
It can be implemented through the following four techniques:
- Establishing control on consistency/coherence of the system status.
- Error management.
- Error recovery through continuation.
- Establishing assertion.
The philosophy of defensive programming is to be cautious about:
- Code developped by others and by yourself.
- User inputs (from a user interface, a file, a network, etc.).
- External services (database, web services, etc.).
- Anything external to the code in general.
In other words, you should always be prepared for the worst and make sure that your code can handle any situation (or at least try to be). Here are some key concepts to defend your code:
-
Input validation – Always assume that inputs can be invalid, malformed, or malicious. Therefore, it is crucial to validate all inputs, especially those coming from external sources such as user input, network data, and files.
-
Avoid assumptions – Do not assume that anything, such as input, file format, or system state, will always be as expected. Instead, assume the worst and prepare for it. For example, rather than assuming a file exists, always check first.
-
Fail-safe defaults – In the case of failure or unexpected conditions, the system should default to a safe state rather than proceeding in an unsafe or insecure manner. For example, if user authentication fails, the system should deny access by default.
-
Defensive use of exceptions – Use exceptions wisely, but don’t swallow exceptions (i.e., catching an exception and doing nothing). Catching exceptions should come with a clear handling strategy.
-
Code contracts – A code contract defines expectations about the inputs, outputs, and state changes of a function. These are typically broken down into:
- Preconditions – Conditions that must be true before a function is executed.
- Postconditions – Conditions that must be true after a function executes.
- Invariants – Conditions that must always be true for a particular object or system. Many modern programming languages support design-by-contract features, which make these conditions explicit.
-
Graceful degradation and error handling – Programs should degrade gracefully when faced with unexpected inputs or failures. For example, instead of crashing, a program could log the error and provide a fallback mechanism.
-
Avoiding global state – Excessive use of global variables or mutable shared state can lead to bugs that are hard to trace. Ensure that changes in one part of the program do not unintentionally affect other parts. Prefer local state or controlled access to shared resources (e.g., using proper synchronization techniques in multi-threaded applications).
-
Assertions – Use assertions to check for conditions that should never occur under normal circumstances.
-
Immutability – Immutable objects are easier to reason about because their state cannot change once created. Where possible, prefer immutable data structures to reduce the likelihood of unintended side effects. In Python, tuples are an example of immutable data structures, whereas lists are mutable.
-
Separation of Concerns – Write code that adheres to the single responsibility principle. Avoid having one function or module perform multiple tasks. This makes the code easier to test, debug, and understand.
3 — Best practices for defensive programming
Defensive programming involves several key practices to ensure robust and resilient code:
-
Always consider edge cases that might break your code, such as empty inputs, maximum values, and other unlikely but possible situations.
-
Use safe defaults by setting your defaults to secure options, like least privilege access in security.
-
Limit exposure by keeping internal functions and variables as private as possible, which is part of encapsulation in object-oriented programming (OOP) and helps limit the scope of errors or misuse.
-
Test your code thoroughly by writing unit tests to check expected functionality and simulate invalid input, exceptions, and failure scenarios to ensure appropriate responses.
-
Fail early by detecting and handling problems as soon as possible in the lifecycle of a program, such as validating input at the boundaries of your system.
-
Lastly, use type checking where applicable, ensuring data types are checked at runtime for dynamic languages or using static type checking tools to detect errors during development.
To go further
Looks like this section is empty!
Anything you would have liked to see here? Let us know on the Discord server! Maybe we can add it quickly. Otherwise, it will help us improve the course for next year!
To go beyond
- Certifiable Software Applications 1.
How to write software that can obtain certifications.