← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 35: Security for Python Developers

Section 1

Security for a Python developer starts with a mindset: assume that input cannot be trusted, that secrets must be protected, and that defaults should be safe. A program that handles data from users, files, networks, or other programs is exposed to that data, and if it treats the data as trustworthy without checking, it may behave in ways its author did not intend. That unintended behaviour can range from a crash, to incorrect output, to disclosure of private information, to unauthorised actions. Thinking about security while designing a program, rather than adding it afterwards, helps you build programs that are robust against both mistakes and deliberate misuse.

A useful way to think about security is in terms of trust boundaries. A trust boundary is a point where data crosses from a trusted context to an untrusted one, or vice versa, such as when a program receives input from a user, reads a file supplied by someone else, or accepts a request over a network. At each boundary, data should be validated, meaning checked to ensure that it is of the expected form, within expected limits, and safe to use for its intended purpose. Validation at boundaries is the single most important security habit, because it prevents untrusted data from reaching sensitive operations in a form that could cause harm.

Another key idea is defence in depth, which means using several layers of protection, so that if one layer fails, another still protects the program. For example, a program might validate input, use safe libraries that handle data correctly, run with minimal permissions, and log suspicious activity, all together. No single layer is perfect, but together they make it much harder for a problem to cause serious harm, because an attacker or a mistake would need to bypass multiple protections, not just one. Defence in depth is a practical approach, because it acknowledges that bugs happen, and it designs programs to be resilient when they do.

Security also involves confidentiality, integrity, and availability, often called the CIA triad in security teaching. Confidentiality means that private data is not disclosed to unauthorised parties. Integrity means that data is not altered without authorisation, and that alterations can be detected. Availability means that a program remains usable by legitimate users, even under heavy load or attack. Those three goals sometimes conflict, such as when strong confidentiality measures make a program harder to use, and balancing them for a particular program's purpose is part of security design. Keeping all three in mind, rather than focusing on only one, leads to more balanced security decisions.

Finally, this chapter covers defensive security only. It teaches how to handle secrets, validate input, choose safe defaults, use established libraries for sensitive operations, and respond to security problems responsibly. It does not teach how to attack systems, bypass controls, or exploit vulnerabilities, because those skills, while they are part of some security professions, are not needed to write defensive, trustworthy Python programs, and they can cause harm if misused. The defensive skills in this chapter are sufficient, and indeed essential, for any developer who writes programs that handle real data for real users.

Check: What is a trust boundary, and why should data be validated when it crosses one?

Section 2

Threat modelling is the practice of thinking, before writing code, about what could go wrong, who might cause it, and what the consequences would be. A threat model does not need to be formal or elaborate; it can be a short list of questions, answered for a particular program. What data does this program handle, and how sensitive is it? Who can supply input to it, and what could they supply that would cause harm? What actions does it perform, and what would happen if those actions were triggered incorrectly? What would be the impact if private data were disclosed, or if the program became unavailable? Answering those questions, even briefly, focuses security effort where it matters most, rather than spreading it evenly over every possible risk.

Assets are the first part of a threat model. An asset is something valuable that a program handles or protects, such as user passwords, personal information, financial records, or simply the program's own ability to run correctly. Identifying assets helps you prioritise: a program handling passwords needs stronger protections than a program handling public, non-sensitive data, because the consequences of a failure are greater. That prioritisation is important, because security effort is limited, and spending it on low-value assets while neglecting high-value ones is a poor use of time. A threat model that lists assets in order of sensitivity guides where to invest effort first.

Threat actors are the second part, though the term should be understood broadly. A threat actor need not be a malicious attacker; it can be a careless user who supplies unexpected input, a faulty program that sends malformed data, or an environmental factor such as a full disk or a network failure. Thinking about non-malicious actors, as well as malicious ones, broadens threat modelling to include robustness, not just attack resistance, and it leads to programs that handle unexpected input gracefully, whether that input was supplied deliberately or accidentally. That broader view is practical, because most security failures in real programs result from mistakes and unexpected input, not from sophisticated attacks.

Attack surface is a third concept: the set of points where a program can be reached by untrusted input, such as command-line arguments, file inputs, network requests, environment variables, and configuration files. A program with a large attack surface, with many inputs from many sources, needs more validation and more careful handling than one with a small surface, such as a program that only reads a single, trusted configuration file. Reducing attack surface, where possible, by removing unnecessary inputs, disabling unnecessary features, or restricting who can supply input, is a powerful security measure, because it removes opportunities for problems to arise, rather than trying to handle every possible input safely.

Finally, threat modelling should be revisited as a program evolves. New features add new assets, new inputs, and new actions, each of which changes the threat model, and a model that was accurate for an early version may be outdated for a later one. Revisiting the model when adding a feature, and updating security measures accordingly, keeps security aligned with the program's actual behaviour, rather than with its original design.

Check: Why should threat modelling consider careless users and faulty programs, not only malicious attackers?

Section 3

Input validation is the practice of checking data before using it, and it is the most fundamental defensive technique in this chapter. Validation means confirming that data matches expectations: that it is of the right type, within an acceptable range, of an acceptable length, and in an acceptable format, for its intended use. For example, a program expecting an age might check that the value is a number, that it is within a plausible range, and that it is not a string or a negative number. Those checks, performed before the data is used, prevent a wide range of problems, from crashes due to unexpected types, to incorrect calculations due to out-of-range values, to security issues due to data that changes the meaning of an operation.

Validation should be performed at trust boundaries, as discussed earlier, and it should be strict by default, in the sense of allowing only known-good data, rather than trying to block known-bad data. That allowlist approach, where a program defines what is acceptable and rejects everything else, is more robust than a denylist approach, where a program tries to list everything that is bad and allows everything else, because a denylist can never be complete: there will always be a bad input that was not anticipated, which will then be allowed. An allowlist, by contrast, fails safe, in the sense that an unexpected input is rejected, even if it turns out to have been harmless, which is a safer failure mode for security-critical data.

Validation should also be performed as close as possible to where data enters a program, rather than deep inside its logic, where a forgotten check can allow bad data to travel far before causing a problem. Early validation, with clear error messages naming what was expected and what was received, helps users correct their input, and it helps developers diagnose problems, because a failure occurs at the boundary, where the bad data entered, rather than deep inside processing, where its origin is obscured.

It is important to validate data for its intended use, not just in general. A value that is valid as a filename may not be valid as a database key, and a value that is valid as text may not be valid as a number. Validation rules should therefore be specific to how data will be used, and they should be applied again if data is used in a new context, even if it was validated for its original context. That context-specific validation prevents a common mistake, where data validated for one purpose is reused for another, more sensitive purpose, without revalidation, allowing it to cause harm in the new context that it could not cause in the old one.

Finally, validation is not a substitute for safe handling. Even validated data should be handled with safe libraries and safe operations, as discussed later, because validation can reduce risk but cannot eliminate it entirely, especially for complex data such as text that may contain special characters or encoding issues. Validation and safe handling together, as layers in defence in depth, provide stronger protection than either alone, and they should both be used for sensitive operations, such as building queries, constructing file paths, or rendering output, where untrusted data could change meaning if handled naively.

Check: Why is an allowlist approach to validation safer than a denylist approach?

Section 4

Secrets are values that must be kept private, such as passwords, tokens, private keys, and other credentials, and handling them correctly is a critical defensive skill. The first rule for secrets is never to write them into source code. Code is often shared, copied, or stored in version control, where a secret written into it can be exposed to people who should not see it, and once exposed, a secret cannot be made secret again simply by deleting it from the code, because copies may remain in history or in other locations. That rule applies even to code that is currently private, because private code can become public, or can be shared with a colleague who should not have access to a particular secret, and a secret in code is a secret waiting to be exposed.

Instead of writing secrets into code, programs should read them from a secure source at run time, such as an environment variable set only on the computer where the program runs, or a protected file with permissions that limit who can read it, or a dedicated secret management service, where one is available. Those sources keep secrets out of code and out of version control, and they allow different secrets to be used in different environments, such as development and production, without changing code. That separation, between code and secrets, is a fundamental security practice, and it should be followed even for small, personal projects, because it builds habits that scale to larger, more sensitive projects.

Secrets should also never be logged, printed, or included in error messages, because logs and error messages are often less protected than the secret's original source, and they may be shared for debugging, stored in files, or sent to monitoring services, where they could be seen by unauthorised people. A program that logs that authentication failed is useful; a program that logs the password that was tried is a leak, even if the log is currently private, because logs tend to be copied, archived, and shared more widely than expected. Avoiding secrets in output, in all forms, is a simple rule that prevents a common and serious class of leaks.

When a secret is no longer needed, it should be cleared from memory where possible, and it should be rotated, meaning replaced with a new secret, if it may have been exposed. Rotation is important because a secret that was exposed, even briefly, should be treated as compromised, and continuing to use it risks unauthorised access by anyone who obtained it during its exposure. Planning for rotation, by making secrets easy to change without changing code, and by documenting how to rotate them, is part of secret management, and it should be considered from the start, not only after an exposure occurs.

Finally, if a secret is accidentally exposed, the response should be prompt and thorough: revoke the exposed secret, replace it with a new one, check for unauthorised use during the exposure period, and review how the exposure happened, to prevent it recurring. Handling secrets well, and responding promptly when they are exposed, are both part of defensive security.

Check: Why should secrets never be written into source code, even for a currently private project?

Section 5

Safe defaults mean that a program, when run without explicit configuration, behaves in the safest reasonable way, rather than in the most convenient or most powerful way. For example, a program that can listen for network connections should, by default, listen only on the local computer, not on all network interfaces, unless the user explicitly configures it to listen more broadly, because listening broadly exposes the program to remote users who may not be authorised. Similarly, a program that can delete files should, by default, require confirmation, or operate in a safe mode, rather than deleting without warning, because the safe default protects users from accidents, even if it requires an extra step for intentional deletions.

Safe defaults are important because many users never change defaults. They install a program, run it, and use it as configured, without reading documentation about every option, and they trust that the program's defaults are sensible. If a default is unsafe, such as allowing unauthenticated access, or overwriting files without warning, many users will be exposed, not because they chose an unsafe configuration, but because they did not know they needed to change it. Designing defaults to be safe therefore protects the majority of users, including those who are least experienced and least likely to recognise an unsafe default.

Choosing safe defaults involves thinking about the worst reasonable outcome of a default behaviour, and whether that outcome is acceptable without explicit user action. If a default could lead to data loss, unauthorised access, or disclosure of private information, it is not safe, even if it is convenient, and it should be changed to a safer alternative, with the convenient but risky behaviour available only as an explicit option, clearly documented as risky. That principle, of requiring explicit opt-in for risky behaviour, is sometimes called secure by default, and it is a standard expectation for well-designed software, because it aligns convenience with informed consent: users who choose a risky option do so knowingly, rather than by default.

Safe defaults also apply to permissions and access. A program should run with the minimum permissions it needs, and should create files with permissions that restrict access to authorised users, rather than making files readable by everyone by default, unless the files are intended to be public. A program that creates a file containing sensitive data with world-readable permissions, by default, exposes that data to anyone on the same computer, even if the program itself is secure. Setting restrictive permissions by default, and allowing broader permissions only when explicitly requested, protects data in shared environments, where multiple users may share a computer or a filesystem.

Finally, safe defaults should be documented, so that users understand what a program does by default, and what they are changing when they override a default. Documentation that states defaults clearly, in help text and in a readme, helps users make informed decisions, and it helps reviewers and auditors check that defaults are indeed safe. A program whose defaults are safe but undocumented may still be used unsafely, because users may override a safe default without realising its purpose, thinking they are merely customising behaviour. Documenting defaults, with reasons where they matter, completes the secure-by-default design.

Check: Why are safe defaults especially important given that many users never change them?

Section 6

Authentication is the process of verifying who a user is, and authorisation is the process of deciding what that user is allowed to do, and distinguishing between them is important for secure design. Authentication answers the question, are you who you claim to be, typically by checking something the user knows, such as a password, something they have, such as a device, or something they are, such as a fingerprint. Authorisation answers a different question, are you allowed to perform this action, based on the authenticated identity and on the program's rules, such as whether a user may view, edit, or delete a particular item.

Passwords, where used, should never be stored in plain text. Instead, they should be stored as hashes produced by a slow, salted hashing function designed for passwords, using an established library, not a general-purpose hash function and not a custom scheme invented for the project. That distinction matters, because general-purpose hashes are designed to be fast, which makes them easier for an attacker to guess passwords by trying many candidates quickly, while password hashing functions are designed to be slow, with a unique salt per password, making guessing much harder and preventing precomputed attacks. Using an established password hashing library, and following its documentation, is the safe approach, and inventing a password storage scheme is a common and dangerous mistake that should be avoided.

Session handling, for programs that keep users logged in, also requires care. A session should be identified by a random, unpredictable token, generated with a secure random source, not by a predictable value such as a username or a sequential number, because a predictable token can be guessed, allowing an attacker to impersonate a user. Session tokens should expire after a reasonable period, and should be invalidated on logout, so that a stolen token cannot be used indefinitely. Those session practices, while they may seem like details, are essential for authentication security, because a session token is, in effect, a temporary password, and it must be protected as such.

Authorisation should be checked on every action, not just at login, and it should be checked on the server or in the program's logic, not only in the user interface, because interface checks can be bypassed by a user who interacts with the program directly, rather than through its intended interface. For example, a program that hides a delete button from users who are not allowed to delete, but that does not check authorisation when a delete request is received, is insecure, because a user can send a delete request directly, bypassing the hidden button. Checking authorisation for every sensitive action, based on the authenticated user and the item involved, prevents that bypass, and it is a fundamental rule of secure design.

Finally, authentication and authorisation failures should be handled carefully, without revealing unnecessary information. An error message that says a username does not exist, versus one that says a password is incorrect, reveals whether a username is valid, which can help an attacker enumerate valid usernames. A generic message, such as authentication failed, without distinguishing between unknown user and wrong password, avoids that leakage, while still informing a legitimate user that their attempt failed, though they will need to check both their username and password.

Check: Why should authorisation be checked for every sensitive action, not just in the user interface?

Section 7

Data handling security involves protecting data as it is stored, transmitted, and processed, and it starts with minimising the data a program collects and retains. A program should collect only the data it needs for its purpose, retain it only as long as needed, and delete it securely when it is no longer needed, because data that is not collected cannot be leaked, and data that is deleted cannot be stolen later. That minimisation principle, sometimes called data minimisation, is both a security practice and a privacy practice, because it reduces the impact of a breach: if a program holds less sensitive data, a breach exposes less, and users' privacy is better protected, even when other protections fail.

When data must be stored, it should be stored with appropriate protections, based on its sensitivity. Sensitive data, such as passwords, should be hashed, as discussed earlier, not stored in plain text. Other sensitive data, such as personal information, should be stored with access controls that limit who can read it, and, where appropriate, encrypted, so that even if storage is accessed without authorisation, the data remains unreadable without the encryption key. Encryption, like password hashing, should use established libraries and standard algorithms, not custom schemes, because custom encryption is notoriously easy to get wrong, in ways that are not obvious to its author but that undermine its protection entirely.

When data is transmitted, such as over a network, it should be protected in transit, typically by using encrypted connections, where the transport itself provides confidentiality and integrity, rather than by sending sensitive data in plain text. A program that sends passwords, tokens, or personal data over an unencrypted connection exposes that data to anyone who can observe network traffic, which, on shared networks, may be many people. Using encrypted transport, and verifying the identity of the other end of the connection, through certificates or other established mechanisms, prevents eavesdropping and tampering in transit, and it is a standard expectation for any program that handles sensitive data over a network.

Processing data securely also means avoiding unnecessary copies of sensitive data. A program that copies sensitive data into logs, temporary files, error messages, or backups, creates additional places where that data must be protected, and where it may be forgotten when the original is deleted. Minimising copies, by processing data in memory where possible, by avoiding logging sensitive values, as discussed earlier, and by ensuring that temporary files containing sensitive data are deleted promptly and securely, reduces the attack surface for data exposure, and it simplifies compliance with data protection expectations, which often require knowing where sensitive data lives.

Finally, data handling security includes planning for deletion. When data is no longer needed, deleting it should mean more than removing a reference to it, if the data is sensitive, because deleted files may still be recoverable from storage until overwritten. Secure deletion, where appropriate, involves overwriting data or using storage features that ensure it cannot be recovered. For most Python programs, the practical steps are to delete files promptly, to avoid unnecessary backups of sensitive data, and to document retention and deletion policies, so that users and maintainers understand how long data is kept and how it is removed when no longer needed.

Check: Why does collecting only necessary data improve security even if other protections fail?

Section 8

File handling security involves treating file paths and file contents as untrusted input, especially when they come from users or from external sources. A file path supplied by a user should be validated, to ensure that it refers to a location the program intends to allow, rather than to a sensitive location elsewhere on the system. That validation is important because a path can contain components that move up the directory tree, or that refer to absolute locations, potentially allowing a program that intended to read a file in a particular folder to read a file elsewhere, such as a configuration file or a password file, if it simply joins user input to a base folder without checking the result. Checking that a resolved path remains within the intended folder, after resolving any such components, is a standard defensive check for file handling.

File contents should also be treated with care, especially when they are parsed, executed, or rendered. A file that is expected to contain data, such as CSV or JSON, should be parsed with a parser for that format, not executed as code, because executing file contents as code would allow a malicious file to run arbitrary operations, with the program's permissions. Similarly, a file that contains text to be displayed should be displayed as text, with appropriate escaping if it will be rendered in a context where special characters have meaning, such as a web page, where unescaped text could be interpreted as markup or script by a browser.

File permissions, as discussed for safe defaults, also matter for file handling. Files created by a program should have permissions that restrict access to authorised users, especially if they contain sensitive data, and files read by a program should be checked for ownership and permissions where appropriate, to ensure that they have not been replaced by an unauthorised user with a malicious version.

Temporary files deserve special attention, because they are often created with predictable names, in shared folders, where another user could create a file with the same name first, causing the program to write to or read from a file controlled by someone else. Using secure temporary file creation, which generates unpredictable names and creates files with restrictive permissions atomically, prevents that kind of interference, which is known as a symlink or race attack on temporary files in security literature, though the details are beyond this chapter. The practical lesson is to use standard library tools for temporary files, rather than inventing temporary file names by hand, because those tools handle unpredictability and permissions correctly.

Finally, file handling security includes handling file size and quantity limits. A program that reads files without limits may be overwhelmed by a very large file, consuming memory or disk space, or by a very large number of files, consuming time and resources, which can be exploited to deny service, as discussed for concurrency. Setting reasonable limits on file size, on the number of files processed, and on total processing time, and reporting a clear error when a limit is exceeded, protects a program from resource exhaustion due to unexpected or malicious files, while still allowing legitimate files to be processed normally, provided the limits are set generously above normal sizes.

Check: Why should a program check that a user-supplied file path stays within its intended folder?

Section 9

Web-facing Python programs, even simple ones, face security considerations that go beyond general input validation, because they are exposed to remote users who may supply crafted requests. One fundamental principle for web programs is to never trust client-supplied data, including data in URLs, forms, headers, and cookies, because a client can send any data it chooses, regardless of what a web form or a user interface normally allows. A web program should validate all client data at the server, as discussed for input validation generally, rather than relying on client-side checks, which can be bypassed by sending requests directly, without using the intended interface. Server-side validation is the only validation that matters for security, because it cannot be bypassed by a determined client.

Output encoding is another fundamental web security practice. When a program includes user-supplied data in a web page, it should encode that data so that it is displayed as text, not interpreted as markup or script by the browser, unless that interpretation is explicitly intended and safe. That encoding, performed by a web framework or a templating library that encodes by default, prevents a class of problems where user data changes the structure of a page, potentially affecting other users who view that page.

Database interaction in web programs should use parameterised queries, where user data is passed as parameters to a query, separate from the query's structure, rather than by constructing query text that includes user data directly. That separation ensures that user data is treated as data, not as part of the query's structure, so it cannot change what the query does, regardless of its contents. That principle, of separating data from structure, is the same as the data-versus-code distinction discussed for files, applied to databases, and it is a fundamental defence for web programs that store and retrieve data. Using a database library's parameter mechanism, rather than building query text by hand, is the standard way to achieve that separation, and it should be used for every query that includes user-supplied data, without exception.

Session and cookie security, for web programs that maintain user sessions, follows the principles discussed for authentication generally, with additional web-specific details. Session tokens in cookies should be marked so that they are only sent over encrypted connections, and so that they are not accessible to scripts running in a browser, where possible, because those markings reduce the risk of token theft through network eavesdropping or through script access.

Finally, web programs should follow the principle of least privilege in their deployment, by running with minimal permissions, by separating public-facing components from sensitive components, and by keeping frameworks and libraries up to date, because web frameworks are common targets, and vulnerabilities in them are regularly discovered and fixed. Keeping dependencies updated, as discussed for packaging, is particularly important for web programs, because an outdated framework with a known vulnerability can be exploited remotely, even if the program's own code is correct. That maintenance, combined with server-side validation, output encoding, parameterised queries, and secure session handling, forms a layered defence for web-facing Python programs.

Check: Why is server-side validation essential even when a web form already checks input in the browser?

Section 10

Safe libraries and dependencies are a cornerstone of defensive security, because most programs rely on external code, and vulnerabilities in that code become vulnerabilities in the program. Choosing dependencies carefully, preferring well-maintained, widely used libraries with a track record of security fixes, over obscure or unmaintained libraries, reduces the risk of introducing a vulnerable component, because widely used libraries are more likely to have their vulnerabilities discovered and fixed promptly, and their maintainers are more likely to respond to security reports. That preference for established libraries applies especially to security-sensitive operations, such as cryptography, authentication, password hashing, and parsing untrusted data, where a flawed implementation can undermine an entire program's security, even if the program's own code is correct.

Keeping dependencies up to date is equally important, because even a well-maintained library will have vulnerabilities discovered over time, and those vulnerabilities are fixed in new versions, which a program will only benefit from if it updates. Updating dependencies should be done deliberately, with testing, as discussed for packaging, rather than blindly, because an update can change behaviour, but it should not be neglected indefinitely, because neglect leaves known vulnerabilities in place, which can be exploited by anyone who knows about them, since vulnerability information is often public after a fix is released. That public nature of vulnerability information means that an outdated dependency is not just a theoretical risk; it is a known, exploitable risk, once its vulnerability is disclosed.

Dependency auditing, through tools that check a project's dependencies against databases of known vulnerabilities, provides a systematic way to stay informed, rather than relying on manual checking or on news. Those tools, which are available for Python projects, can be run as part of development, or as part of continuous integration, to flag dependencies with known vulnerabilities, so that they can be updated promptly. Using such a tool, and acting on its findings, is a practical security habit for any project with dependencies, and it complements the packaging practices discussed earlier, by adding a security dimension to dependency management.

It is also important to minimise dependencies, as discussed for packaging, because each dependency is a potential source of vulnerabilities, and a program with many dependencies has a larger attack surface, in terms of code it trusts, than one with few. A dependency that is used for a trivial task, such as a simple string operation that could be done with the standard library, adds risk without proportionate benefit, and it should be removed if the standard library can do the job safely. That minimisation, combined with careful choice and regular updating, keeps a program's dependency risk manageable, rather than allowing it to grow unchecked as features are added.

Finally, dependencies should be obtained from trusted sources, such as the official package index, with verification, rather than from untrusted websites or from copies passed around informally, because a dependency obtained from an untrusted source could have been altered to include malicious code, even if its name matches a legitimate library. Trusted sources, verified integrity, and careful naming checks together form a basic defence against supply chain attacks.

Check: Why does keeping dependencies up to date matter even if a program's own code is correct?

Section 11

Cryptography is the science of protecting information through mathematical techniques, and for most Python developers, the key lesson is to use established libraries, not to invent your own methods. A general-purpose hash function turns data into a short fingerprint, useful for checking whether data has changed, but it is not encryption, because it cannot be reversed to recover the original data. Encryption, by contrast, turns readable data into unreadable data using a key, and the same key, or a paired key, can reverse the process. Confusing hashing with encryption, or using one where the other is needed, is a common mistake that can leave data unprotected in ways that are not obvious until it is too late.

Symmetric encryption uses the same key to encrypt and decrypt, and it is suitable when both parties can share a key securely in advance. Asymmetric encryption uses a pair of keys, one public and one private, where data encrypted with one can only be decrypted with the other, allowing secure communication without first sharing a secret key. For most application developers, however, the choice is made by the library or protocol they use, such as an encrypted transport protocol, rather than by implementing encryption directly, which is the safer path.

Key management is often harder than the cryptography itself. A key that is lost cannot decrypt data, while a key that is exposed allows anyone who has it to decrypt data, so keys must be stored securely, backed up where recovery matters, and rotated if exposure is suspected, following the secret management principles discussed earlier. Hard-coding a key into source code, as with other secrets, is a serious mistake, because code is shared and copied, and a key in code is effectively public. Reading keys from a secure source at run time, with restricted permissions, and never logging them, are basic key management habits that protect encryption's value.

Another important lesson is that cryptography does not, by itself, guarantee security. An encrypted message can still be intercepted and replayed, if replay protection is not included, or it can be sent to the wrong recipient, if identity is not verified, or it can be stored alongside its key, defeating its purpose entirely. Security requires thinking about the whole system, including authentication, integrity, key management, and access control, not just about encrypting data in isolation. A program that encrypts data correctly but stores the key next to it, or that does not verify who it is communicating with, has not achieved the security its author intended, despite using cryptography.

Finally, when you need cryptography, choose a well-maintained library, follow its documentation and examples, and avoid low-level cryptographic primitives unless you have specific expertise and a clear reason to use them. High-level libraries provide safe defaults, such as choosing appropriate algorithms and key sizes, and they handle details that are easy to get wrong, such as random number generation and padding. That reliance on established, high-level tools, rather than on custom implementations, is the defensive approach to cryptography, and it is consistent with the broader lesson of this chapter: use trusted tools for sensitive operations, and focus your own effort on design, validation, and key management, where your judgement matters most.

Check: Why should developers use established cryptography libraries instead of inventing their own methods?

Section 12

Random numbers play a security role in many programs, for generating tokens, passwords, session identifiers, and other unpredictable values, and using the right kind of randomness matters. Python provides a general-purpose random module, suitable for simulations, games, and other non-security purposes, where unpredictability is not critical, but it is not suitable for security, because its output can be predicted if its internal state is known or guessed. That predictability is not a flaw in the general module; it is a design choice, because simulations often need reproducible sequences, which security randomness must not provide. Using a general random generator for a security token, however, is a mistake, because an attacker who can predict the token can impersonate a user or bypass a check that relies on that token's secrecy.

Python also provides a secrets module, designed specifically for security, which generates unpredictable values using a secure source provided by the operating system. That module should be used for tokens, passwords, session identifiers, and any other value whose unpredictability matters for security. Its functions are simple to use, and they are documented as suitable for security, which makes the choice clear: if a random value will be used as a secret, or as part of an authentication or authorisation decision, use the secrets module, not the general random module. That rule, applied consistently, prevents a class of vulnerabilities where predictable tokens undermine otherwise sound authentication.

It is also important to understand what makes randomness secure. Secure randomness comes from entropy, which is unpredictability derived from physical or system sources, such as hardware events, timing, and other inputs that an attacker cannot easily predict or control. A secure random generator collects entropy from those sources and produces output that is statistically random and, crucially, unpredictable even to someone who has seen previous output. A non-secure generator, by contrast, produces output from a deterministic algorithm seeded with a value that may be predictable, such as the current time, which an attacker can guess or brute-force, especially if the seed space is small.

Testing randomness is difficult, because a single sequence of numbers cannot prove unpredictability, and statistical tests can only show that output looks random, not that it cannot be predicted by someone who knows the generator's state. That difficulty is another reason to rely on established secure generators, rather than implementing your own, because a custom generator may pass statistical tests while still being predictable to an attacker who understands its design. Trusting the operating system's secure source, through the secrets module, delegates that difficult problem to experts and to well-tested implementations, which is the defensive choice.

Finally, random values used for security should be long enough to resist guessing, with enough entropy that brute-force guessing is infeasible. A short token, such as a four-digit code, can be guessed quickly, even if generated securely, because there are only ten thousand possibilities, while a long token, with many characters from a large alphabet, has so many possibilities that guessing is impractical. Choosing token length based on the attacker's ability to guess repeatedly, and on rate limiting that slows guessing, as discussed for authentication, balances usability against security, and it should be considered alongside the choice of generator, because a secure generator with a too-short output still provides weak protection.

Check: Why is Python's general random module unsuitable for generating security tokens?

Section 13

Logging is essential for debugging and monitoring, but logs can become a security risk if they contain sensitive information, because logs are often stored, copied, and shared more widely than the data they describe. A log that records a password, a token, a private key, or personal data, even for debugging, creates a second copy of that sensitive data, in a file that may have weaker protections than the original source, and that may be accessible to people who should not see the sensitive data. That risk is why the rule from earlier chapters, never log secrets, should be treated as absolute, not as a guideline to follow when convenient, because logs have a habit of outliving their original purpose and reaching unexpected audiences.

Personal data in logs deserves similar care. A log that records a user's full name, address, or other personal details, alongside an error, may be useful for debugging a specific user's problem, but it also creates a record of personal data that must be protected under privacy expectations, and that may be subject to deletion requests or to breach notification if exposed. Minimising personal data in logs, by recording identifiers such as a user number, rather than names, or by recording counts and error types rather than full records, reduces that risk while still providing enough information to diagnose problems. When personal data is genuinely needed in a log, for a specific investigation, it should be logged deliberately, with awareness of its sensitivity, and removed when no longer needed, rather than logged routinely for every request.

Log levels and destinations also affect security. Debug logs, which contain detailed information for developers, may include sensitive data that production logs should not, because debug logs are often more verbose and less carefully reviewed. A program that logs sensitive data at debug level in development, and then accidentally enables debug logging in production, will leak that data in production, where logs may be more widely accessible. Separating debug logging from production logging, by ensuring that debug level is never enabled in production without review, and by reviewing debug log statements for sensitive data before release, prevents that accidental leakage, which is a common cause of log-related security incidents.

Log files themselves need protection, through file permissions that restrict who can read them, and through retention policies that delete old logs after a reasonable period, rather than keeping them indefinitely. A log file that is world-readable, or that is kept forever, increases the window during which sensitive data in it could be exposed, whether through a breach, a misconfiguration, or a simple mistake in sharing files. Setting restrictive permissions by default, as discussed for safe defaults, and deleting logs when they are no longer needed for debugging or auditing, are basic log security habits that complement the content rules above, by protecting whatever sensitive data logs do legitimately contain.

Finally, logs are also a security tool, because they record suspicious activity, such as repeated failed logins, unusual access patterns, or errors that may indicate an attack, and reviewing logs for those patterns is part of security monitoring. That balance, between visibility and restraint, is a practical security design decision, and it should be revisited as a program evolves and as its logging needs change.

Check: Why should debug logs never be enabled in production without review for sensitive data?

Section 14

Error handling, from a security perspective, involves giving users enough information to understand and fix a problem, without giving attackers information that helps them exploit a program. A detailed error message that includes internal details, such as file paths, database structure, library versions, or stack traces, may help a developer debug a problem, but it also helps an attacker understand a program's internals, identify its weaknesses, and craft an attack tailored to its specific setup. That information leakage, through error messages, is a common and avoidable security issue, and it is prevented by distinguishing between errors shown to users and errors recorded for developers.

For users, error messages should be generic but helpful, stating what went wrong in terms of the user's action, and what they can do next, without revealing internal details. For example, a message that says a file could not be found, and suggests checking the filename, is helpful, while a message that includes the full internal path, the database query, and a stack trace is not only confusing for a user, but also revealing for an attacker. That generic user message should be accompanied by a detailed log entry, recorded for developers, with the full error details, so that debugging is still possible, without exposing those details to users who should not see them. That separation, between user-facing messages and developer logs, is a standard security practice for error handling.

Stack traces deserve special attention, because they reveal a program's internal structure, including file names, function names, and line numbers, which can help an attacker map a program's code and identify vulnerable components. In development, stack traces are invaluable for debugging, and they should be shown freely to developers. In production, stack traces should never be shown to users, and they should be logged securely, with access restricted to developers, rather than displayed in a web page, an API response, or a command-line output that a user could copy and share. Configuring a program or a framework to hide stack traces in production, while still logging them, is a simple step that prevents a significant information leak.

Error handling should also avoid revealing whether a particular value exists, when that existence itself is sensitive, as discussed for authentication, where a message should not distinguish between an unknown username and a wrong password. Similar logic applies to other operations: a message that says a particular file does not exist, versus one that says access is denied, reveals whether the file exists, which may be sensitive for files that should remain hidden. A generic message, such as access failed, without distinguishing between non-existence and denial, avoids that leakage, while still informing a legitimate user that their request did not succeed, though they may need to check both the name and their permissions.

Finally, error handling should be consistent, so that an attacker cannot infer information from differences in error behaviour, such as timing, message wording, or exit codes, between different kinds of failures. For example, if a program responds quickly to one kind of invalid input and slowly to another, that timing difference may reveal which kind of input was supplied, even if the messages are identical.

Check: Why should detailed stack traces be logged for developers but never shown to users in production?

Section 15

Least privilege is the principle that a program, a user, or a component should have only the permissions it needs to perform its task, and no more, so that if it is compromised or makes a mistake, the damage is limited. For a Python program, least privilege applies at several levels: the operating system user it runs as, the files it can read and write, the network services it can access, and the database permissions it holds. A program that runs as an administrator, with access to all files, when it only needs to read a few files and write a report, violates least privilege, because a bug or an attack in that program could affect the whole system, rather than only its intended files. Running it as a limited user, with access only to its needed files, contains that damage.

Applying least privilege in code means designing components with narrow interfaces and limited capabilities. A function that only needs to read a file should not also be able to delete files, and a component that only needs to process data should not also have access to secrets or to network services, unless it genuinely needs them.

Database permissions illustrate least privilege well. A program that only reads data should connect to its database with a read-only account, not with an account that can modify or delete data, because a bug or an attack that causes a write would then be blocked by the database, rather than succeeding. Similarly, a program that only accesses one table should use an account limited to that table, not an account with access to all tables, because that limits the data exposed if the program is compromised. Those database permission choices, made at deployment time, are as important as code choices, because they provide a second layer of protection, in defence in depth, even if code-level checks fail.

Least privilege also applies to third-party code and libraries. A library that is given broad access, such as access to the filesystem or to network services, when it only needs to perform a calculation, violates least privilege, and it should be questioned, or its access should be limited where possible, through sandboxing or through configuration. While Python does not provide strong sandboxing for libraries by default, being aware of what a library does, by reading its documentation and, where feasible, its code, and by choosing libraries with minimal, well-documented permissions, is a practical application of least privilege to dependencies, complementing the dependency choice practices discussed earlier.

Finally, least privilege should be reviewed as a program evolves, because a component that once needed only read access may later need write access for a new feature, and its permissions should be updated deliberately, with justification, rather than by granting broad access to avoid thinking about the change. Conversely, a component whose permissions are broader than its current needs should have them reduced, when that is discovered, rather than left broad for convenience. That ongoing review, of permissions against actual needs, keeps least privilege meaningful over time, rather than allowing it to drift into broad access through accumulated convenience grants, which is a common way that least privilege erodes in real systems.

Check: How does running a program as a limited user contain damage if the program is compromised?

Section 16

Secure configuration means setting up a program's settings, files, and environment in a way that does not introduce vulnerabilities, and it starts with the principle that configuration files should not contain secrets in plain text, as discussed for secrets generally. A configuration file that contains a password, even if the file itself is protected, risks exposure when the file is copied, backed up, or shared for debugging, and it should instead contain a reference to a secret source, such as an environment variable, or it should be protected with permissions that limit access to the program's user alone.

Configuration files should also be validated, just as other input should be, because a malformed or malicious configuration file can change a program's behaviour in dangerous ways, such as by pointing it at a different database, by enabling debug mode in production, or by disabling security checks. A program that reads configuration without validating it, trusting that the file was written correctly and by an authorised person, is vulnerable if the file is ever modified by someone else, whether deliberately or accidentally. Validating configuration values, checking that they are of the expected type, within acceptable ranges, and consistent with each other, and reporting clear errors for invalid values, prevents a bad configuration from causing a security problem, rather than merely a functional one.

Default configuration, as discussed for safe defaults, should be secure, so that a program run without any configuration file, or with a minimal one, behaves safely, rather than insecurely. That means that security-relevant settings, such as whether authentication is required, whether encryption is enabled, or whether debug mode is on, should default to their secure values, and should only be changed to less secure values through explicit configuration, with a clear understanding of the risk. A program that defaults to no authentication, and requires configuration to enable it, will be deployed insecurely by many users who do not realise they need to configure it, while a program that defaults to authentication, and requires explicit disabling, protects those users by default.

Configuration files should also have restrictive permissions, especially if they contain sensitive settings, such as database addresses, service endpoints, or feature flags that affect security. A configuration file that is world-readable may reveal information about a program's setup, such as internal service addresses or enabled features, which could help an attacker plan an attack, even if it does not contain secrets directly. Setting permissions so that only the program's user can read its configuration, and checking those permissions on startup, with a warning if they are too broad, is a simple secure configuration habit that reduces information leakage through configuration files.

Finally, configuration should be documented, with examples that show secure values, not insecure ones, because users often copy examples verbatim, and an example that shows authentication disabled, or debug mode enabled, for simplicity, may be copied into production without understanding its risk. Documentation examples should demonstrate secure configuration, with notes explaining when and why a less secure setting might be used, such as in a local development environment, and with warnings that it should not be used in production. That documentation practice, of showing secure examples by default, helps users configure programs safely, even when they are learning from examples rather than from detailed security guidance.

Check: Why should a program validate its configuration file instead of trusting it completely?

Section 17

Deserialisation is the process of converting stored or transmitted data back into objects, and it is a security-sensitive operation when the data comes from an untrusted source, because a maliciously crafted serialised object can cause a program to behave in unintended ways when it is deserialised. In Python, the most well-known deserialisation risk involves the pickle format, which can represent complex objects, including objects that execute code when they are unpickled, by design, for trusted data. That design means that unpickling data from an untrusted source is equivalent to running code supplied by that source, with the program's permissions, which is a severe security risk, and it should never be done, regardless of how the data was obtained or how it appears to have been validated, because validation cannot reliably distinguish safe pickle data from malicious pickle data.

The defensive rule for deserialisation is simple: only deserialise data from trusted sources, using formats designed for untrusted data where possible, and never unpickle untrusted data. For data exchange with untrusted sources, formats such as JSON, which represent only basic data types, such as strings, numbers, lists, and dictionaries, and which cannot represent executable code, are far safer, because parsing JSON cannot execute arbitrary code, even if the JSON data is maliciously crafted, though it can still cause other problems, such as excessive memory use if the data is very large, which should be handled with size limits, as discussed for file handling. Choosing JSON, or another data-only format, for untrusted data, is a fundamental defensive choice, and it should be made whenever data crosses a trust boundary.

It is also important to understand that deserialisation risks are not limited to pickle. Other formats and libraries that reconstruct objects from data, such as certain object serialisation formats, or libraries that evaluate expressions from data, can have similar risks, where data is treated as code, or where object construction can trigger unintended side effects. When choosing a format or a library for untrusted data, check its documentation for security warnings, and prefer formats that are explicitly designed for untrusted input, with clear guarantees about what they can and cannot do.

If a program must accept serialised data from a source that is not fully trusted, additional protections are needed, such as verifying the data's origin and integrity, through a signature or a checksum using a key known only to trusted sources, before deserialising it, and even then, using a safe format rather than pickle, where possible.

Finally, deserialisation security should be considered during design, not discovered after a program is built, because changing a data format after deployment, from an unsafe format to a safe one, can be difficult, especially if existing data must be migrated, or if other programs depend on the old format. Choosing a safe format from the start, for any data that might ever come from an untrusted source, even if it currently comes only from a trusted source, is a prudent design choice, because trust boundaries can change over time, as a program is shared, deployed in new contexts, or integrated with new systems. That forward-looking choice, of safe formats by default, prevents a future trust change from becoming a security vulnerability.

Check: Why should untrusted data never be unpickled, even if it appears to have been validated?

Section 18

Keeping software up to date is a security practice, because vulnerabilities are regularly discovered in libraries, frameworks, and in Python itself, and fixes are released as updates, which only protect users who install them. A program that uses an outdated library with a known vulnerability remains vulnerable, even if its own code is correct, because an attacker can exploit the library's vulnerability through the program, as discussed for dependencies generally. Updating, therefore, is not just about gaining new features; it is about closing known holes, and it should be treated as a security task, with priority given to updates that fix security issues, over updates that merely add features or change behaviour in non-security ways.

Updating should be done systematically, not ad hoc, with a process for learning about updates, testing them, and deploying them. Learning about updates can come from subscribing to security advisories for key dependencies, from using dependency auditing tools, as discussed earlier, or from monitoring a package index for new versions of critical libraries. Testing updates involves running the program's test suite, and smoke testing its main features, in a clean environment, to confirm that the update does not break functionality, before deploying it to users. Deploying updates promptly, once tested, ensures that the window during which a known vulnerability remains exploitable is as short as possible, which is important because vulnerability information is often public, and attackers can exploit it quickly after disclosure.

It is also important to distinguish between security updates and feature updates, and to prioritise accordingly. A security update that fixes a serious vulnerability should be deployed as soon as it has been tested, even if it requires a small amount of downtime or a minor version change, because the risk of remaining vulnerable outweighs the inconvenience of updating. A feature update, by contrast, can wait for a convenient time, or can be grouped with other changes, because it does not close a known hole. That prioritisation, based on security impact rather than on novelty, is a practical way to manage update workload, especially for programs with many dependencies, where updates arrive frequently and cannot all be deployed immediately.

Updating Python itself, as well as libraries, is part of this practice, because the interpreter and the standard library also receive security fixes, and an outdated Python version may contain vulnerabilities that libraries cannot work around. Keeping Python up to date, within the range supported by a program's dependencies, and testing a program on new Python versions before adopting them, as discussed for packaging, ensures that the program benefits from interpreter-level security fixes, while still working correctly. That interpreter updating, like library updating, should be planned, not neglected, because an outdated interpreter can become a long-term vulnerability, even if its code is otherwise sound.

Finally, updating should be documented, in changelogs and in release notes, so that users know which vulnerabilities have been fixed, and can decide whether to update promptly, especially if they are running an older version. A program with a routine update process is a program that is more likely to remain secure over time, because security updates are not treated as exceptional events, but as a normal part of maintenance.

Check: Why should security updates be prioritised over feature updates when deciding what to deploy?

Section 19

Incident response is the process of handling a security problem after it has occurred, such as a suspected breach, a leaked secret, or a program behaving in a way that suggests it has been compromised, and having a basic plan for response is part of defensive security, even for small programs. An incident response plan does not need to be elaborate; it can be a short list of steps, such as: stop the affected program if it is causing harm, preserve evidence such as logs, revoke any exposed secrets, assess what data may have been affected, fix the underlying problem, and communicate with affected users if necessary. Having that list written down, before an incident occurs, helps you respond calmly and systematically, rather than panicking or improvising under stress, which can make a problem worse, such as by deleting evidence that is needed to understand what happened.

Preserving evidence is an important first step that is often overlooked in the rush to fix a problem. Logs, configuration files, and copies of affected data, preserved before they are overwritten or deleted, can reveal how an incident occurred, what was accessed, and whether it is still ongoing, which is essential for fixing the root cause and for preventing recurrence. That preservation should be done carefully, by copying evidence to a secure location, rather than by continuing to run a compromised program in a way that overwrites logs or allows further access.

Communication during an incident should be honest, timely, and proportionate. If users' data may have been affected, they should be informed, with clear information about what happened, what data was involved, and what they should do, such as changing a password, without unnecessary technical detail or speculation about facts that are not yet known. That honesty, while difficult, builds trust, because users prefer to hear about a problem from the program's author, with guidance, rather than learning about it later from another source, without guidance. If an incident affects only the author's own data, with no user impact, communication may be limited to a note for future reference, but the incident should still be recorded and learned from, rather than forgotten.

Fixing the root cause, not just the symptom, is essential for incident response. Revoking a leaked secret, for example, fixes the immediate exposure, but if the secret was leaked because it was written into code, the root cause is the practice of writing secrets into code, and that practice must also be fixed, by moving secrets to a secure source and by checking for other secrets in code, otherwise the same leak will recur with a different secret. That root-cause analysis, asking why an incident happened, not just what happened, turns an incident from a one-off crisis into a learning opportunity that improves a program's security permanently.

Finally, after an incident, the response plan itself should be reviewed and updated, based on what worked and what did not, so that future incidents are handled better. That continuous improvement, of both security practices and response plans, is a hallmark of mature security practice, even for small programs, because incidents, while unwelcome, are inevitable over a long enough period, and learning from them is what prevents repetition.

Check: Why is preserving evidence important before fixing a security incident?

Section 20

A security checklist, used during code review and before release, helps ensure that the defensive practices in this chapter are applied consistently, rather than forgotten under time pressure. The first checklist area is input: has all untrusted input been validated, at trust boundaries, with allowlists where appropriate, and has it been handled with safe libraries, such as parameterised queries for databases, and output encoding for web pages, rather than by constructing commands or markup by hand? That input checklist, applied to every input a program accepts, from command-line arguments to files to network requests, catches the most common security issues, which arise from untrusted data reaching sensitive operations without validation or safe handling.

The second checklist area is secrets and data: are secrets kept out of code and logs, stored in secure sources, and rotated if exposed? Is sensitive data minimised, protected in storage and in transit, and deleted when no longer needed? Are file permissions restrictive, and are temporary files created securely? Those questions, asked for each secret and each piece of sensitive data a program handles, ensure that data protection is considered explicitly, rather than assumed, and they help reviewers spot leaks, such as a secret in a log statement, or a sensitive file with broad permissions, that might otherwise be overlooked in a review focused only on functionality.

The third checklist area is authentication, authorisation, and least privilege: are users authenticated securely, with passwords hashed correctly and sessions managed safely? Is authorisation checked for every sensitive action, not just in the interface? Does the program run with minimal permissions, and do its components have only the access they need? Those questions, asked for every sensitive operation, ensure that access control is enforced where it matters, in the program's logic, not just in its presentation, and that a compromise of one component does not automatically grant access to everything, thanks to least privilege.

The fourth checklist area is dependencies and updates: are dependencies chosen carefully, minimised, and kept up to date, with known vulnerabilities addressed promptly? Are libraries used for security-sensitive operations, such as cryptography and password hashing, established and well-maintained, rather than custom implementations? Those questions ensure that a program's security does not depend on untrusted or outdated code, and that its supply chain, of libraries it trusts, is as secure as its own code, which is essential because a program is only as secure as its weakest trusted component.

Finally, the checklist should include documentation and response: is security-relevant behaviour documented, such as defaults, permissions, and configuration, so that users and maintainers understand it? Is there a basic incident response plan, for handling a leaked secret or a suspected breach, and has it been reviewed? Those questions ensure that security is not just about code, but about the whole lifecycle of a program, from design to deployment to incident handling, and they turn a checklist from a one-time review tool into an ongoing practice that keeps a program secure over time, as it evolves and as new threats emerge.

Check: What four areas does the security checklist in this section cover?

Section 21

Programs that use external services, such as web APIs, need to authenticate to those services, usually with a key or token, and handling those service credentials securely follows the same principles as handling user secrets. A service key should never be written into code, should be read from a secure source at run time, should never be logged, and should be rotated if it may have been exposed.

Service keys should also be scoped as narrowly as possible, meaning that a key should grant only the permissions its program needs, for only the resources it needs, rather than broad, administrator-level access. For example, a program that only reads data from a service should use a read-only key, not a key that can also delete data, because a read-only key limits damage if it is exposed or if the program is misused. That scoping, provided by many services through key permissions or roles, is an application of least privilege to external services, and it should be configured deliberately when a key is created, rather than accepting broad default permissions for convenience.

Keys used for different environments should be different. A development key, used while building and testing a program, should not be the same as a production key, used by real users with real data, because development environments are often less protected, with logs, test data, and shared access that increase exposure risk. Separating keys by environment ensures that exposure of a development key does not compromise production data, and it allows development keys to be rotated or revoked without affecting production users. That separation, while it requires managing several keys, is a standard practice for service integrations, and it is worth the small administrative effort.

When a program calls an external service, it should also validate the service's responses, just as it validates other untrusted input, because a service can return unexpected, malformed, or large data, whether due to a bug, a change in the service, or a malicious intermediary, if the connection is not properly secured. Validating response structure, size, and type, before using response data, prevents a malformed response from crashing the program, or from causing it to act on incorrect data, such as displaying wrong information to users or writing wrong data to a database. That validation, applied to external service data as to any other input, is part of defence in depth for service integrations.

Finally, service integrations should handle failures gracefully, without leaking keys or sensitive data in error messages, and without retrying indefinitely in a way that overwhelms the service or that reveals information through timing. A failed service call should be logged generically, with an identifier for the call, rather than with the key or the full request data, so that debugging is possible without exposing credentials. Retries should be bounded and paced, as discussed for concurrency, to avoid turning a transient failure into a flood of requests. Those graceful failure habits, combined with scoped, separated, and securely stored keys, make service integrations both functional and defensible.

Check: Why should a service key grant only the permissions its program actually needs?

Section 22

Personal data is information that can identify a person, directly or indirectly, such as a name, an address, an email, a phone number, or a combination of details that together identify someone, and handling it securely is both a security and a privacy responsibility. A program that handles personal data should collect only what it needs, as discussed for data minimisation, should store it securely, with access controls and, where appropriate, encryption, and should delete it when it is no longer needed for its stated purpose. Those practices protect users' privacy, even if a program is never attacked, because they reduce the amount of personal data at risk, and they demonstrate respect for users' information, which builds trust in a program and its author.

Personal data in code and in tests deserves special attention, because developers often use real personal data for testing, without realising the risk. Test data should, wherever possible, be synthetic, meaning invented for testing, rather than real personal data copied from production, because test environments are often less protected than production, with broader access, less monitoring, and logs that may be shared. Using synthetic test data, with realistic formats but invented values, allows thorough testing without exposing real people's information, and it avoids the problem of test data leaking through a test environment breach, or through a test file accidentally shared or committed to version control.

Anonymisation and pseudonymisation are techniques for reducing identifiability in data used for analysis or testing, but they should be understood correctly, not assumed to make data completely safe. Anonymisation aims to remove identifiers so that a person cannot be identified, while pseudonymisation replaces identifiers with artificial ones, so that data can still be linked across records without revealing the original identifier directly. Both techniques can reduce risk, but neither guarantees that a person cannot be re-identified, especially when data contains many details that together narrow down identity, such as rare combinations of attributes. Treating anonymised data as if it were completely non-personal, without considering re-identification risk, is a mistake, and anonymised data should still be protected with appropriate access controls, unless a careful assessment shows that re-identification is truly infeasible.

Access to personal data should be logged, where appropriate, so that it is possible to know who accessed what, and when, for auditing and for investigating suspected misuse. That access logging, while it creates its own log data that must be protected, as discussed earlier, provides accountability, because it deters unauthorised access by making it visible, and it allows investigation if a breach is suspected, by showing which records were accessed. Balancing access logging against log privacy, by logging identifiers and actions rather than full personal data generally, achieves accountability without creating a second, less protected copy of personal data in logs.

Finally, personal data handling should be documented, in a program's privacy notes or documentation, explaining what personal data is collected, why it is needed, how long it is kept, and how users can request its deletion or correction, where applicable. Documentation alone does not protect data, but it makes protection intentional, rather than accidental, and it provides a basis for reviewing data handling practices as a program evolves and as its data collection changes over time.

Check: Why should test data be synthetic rather than real personal data copied from production?

Section 23

Security code review is a focused review of code for security issues, complementing functional review, and it follows a checklist similar to the one discussed earlier, but applied to specific code changes. A security reviewer asks, for each change, what new input it accepts, what new actions it performs, what new data it handles, and what new dependencies it introduces, because each of those is a potential new attack surface or a new source of risk.

A security review should start with input handling, checking that all new inputs are validated, that validation uses allowlists where appropriate, and that validated data is handled safely, with parameterised queries, output encoding, and safe file path handling, as discussed throughout this chapter. The reviewer should look for places where user data is used to construct commands, queries, paths, or output, without safe handling, because those are the places where untrusted data can change meaning and cause harm. That search, for unsafe uses of untrusted data, is the core of security review, and it requires tracing data flow from its entry point to its use, rather than merely checking that validation exists somewhere, because validation in one place does not protect a use in another place that bypasses it.

Secrets and sensitive data should also be checked in review, by searching for hard-coded secrets, for secrets in logs, and for sensitive data in error messages, test data, or documentation examples. Those searches, which can be partly automated with tools that scan for patterns resembling secrets, catch common mistakes that are easy to make and hard to notice in ordinary review, because a secret in code looks like any other string, unless the reviewer is specifically looking for it. Automating secret scanning, as part of review or continuous integration, provides a safety net, but it does not replace human review, because a secret may not match a known pattern, or sensitive data may be disguised as ordinary data, requiring human judgement to recognise.

Authentication and authorisation changes deserve particular scrutiny in security review, because a mistake there can grant unauthorised access, even if the rest of the code is correct. A reviewer should check that authorisation is enforced for every new sensitive action, that authentication is handled through established libraries, and that session handling, password handling, and error messages follow the practices discussed earlier, without leaking information or allowing bypass. Those checks, applied to any change that touches authentication or authorisation, even indirectly, such as by adding a new endpoint or a new data field, prevent security regressions that might otherwise be introduced by a change whose primary purpose was functional, not security-related.

Finally, security review should be kind and educational, not adversarial, because its goal is to improve code and to teach secure practices, not to blame authors for mistakes. A reviewer who explains why a pattern is risky, suggests a safe alternative, and links to relevant guidance, helps the author learn and helps future code avoid the same issue, while a reviewer who merely rejects code without explanation creates frustration and discourages security review altogether. That educational approach, combined with a clear checklist and, where possible, automated scanning, makes security review a sustainable practice, even for small teams or solo developers, who can apply the checklist to their own code before sharing it.

Check: Why should security review trace data flow from entry to use, rather than just checking that validation exists somewhere?

Section 24

Frameworks and libraries often provide secure defaults, designed to protect programs that use them, and disabling those defaults without understanding their purpose is a common security mistake. For example, a web framework may encode output by default, to prevent user data from being interpreted as markup, or it may protect against cross-site request forgery by default, through tokens that verify that a request came from the program's own forms, rather than from a malicious site. Disabling those protections because they interfere with a particular feature removes a layer of defence, and it should only be done after understanding what protection is being removed, why it is needed, and what alternative protection will replace it, if any. That understanding, documented in code comments or in a review, ensures that disabling a default is a deliberate, informed decision, not a casual shortcut.

Secure defaults in frameworks also include settings such as requiring authentication for administrative actions, using encrypted cookies, setting restrictive file permissions, and validating input types, each of which protects a program in ways that may not be obvious to a developer focused on functionality. A developer who overrides a framework default to make development easier, such as disabling authentication in a development environment, should ensure that the override does not leak into production, by using environment-specific configuration, with production defaults remaining secure, as discussed for secure configuration.

It is also important to understand that framework defaults evolve, as new vulnerabilities are discovered and new protections are added, and keeping frameworks up to date, as discussed for dependencies, ensures that a program benefits from improved defaults, rather than relying on outdated protections. A framework version from several years ago may lack protections that are standard in current versions, such as improved output encoding, stronger session handling, or better defaults for headers that control browser behaviour. Updating frameworks, with testing, as part of routine maintenance, therefore improves security not only by fixing known vulnerabilities, but also by adopting improved defaults that were not available when the program was first written.

When a framework offers a choice between a secure option and a less secure option, the secure option should be chosen by default, even if it requires more work, such as configuring certificates for encrypted connections, or handling tokens for request verification, because the work required is a one-time cost, while the protection it provides is ongoing. Choosing a less secure option for convenience, such as disabling encryption for internal traffic that is assumed to be safe, should be justified by a careful assessment that the traffic truly is isolated and cannot be observed, rather than by an assumption that internal networks are always safe, because internal networks can be compromised, or can be misconfigured, exposing traffic that was assumed private. That careful assessment, documented and reviewed, is part of secure framework use.

Finally, framework documentation should be read for security guidance, not just for functionality, because frameworks often include a security section, explaining their protections, their defaults, and common mistakes to avoid, which is invaluable for developers using the framework for the first time. Reading that guidance, and following it, is a shortcut to secure framework use, because it distils the framework maintainers' security experience into practical advice, which would otherwise take years of trial and error to learn.

Check: Why should disabling a framework's secure default be a deliberate, documented decision?

Section 25

Security testing, in a defensive sense, means testing that a program handles untrusted input safely, that its protections work as intended, and that its error handling does not leak information, without attempting to attack live systems or to bypass controls in ways that could cause harm. That testing includes validation tests, where a program is given invalid, malformed, or boundary-value inputs, and its behaviour is checked to ensure that it rejects them safely, with clear errors, rather than crashing, producing incorrect output, or performing unintended actions. Those validation tests, which use ordinary test data, not attack payloads, are a standard part of testing any program that accepts input, and they are particularly important for security, because they verify that validation, the first layer of defence, actually works for the inputs it claims to handle.

Security testing also includes tests for authentication and authorisation, where a program is tested with different users, including users without permission, to confirm that unauthorised actions are denied, and that authorised actions succeed, for the correct users and the correct items. Those tests, which use test accounts with known permissions, verify that authorisation checks are present and correct, without needing to attempt to bypass them through illicit means. A test that checks that a user without delete permission cannot delete, by attempting the action through the program's normal interface with that user's credentials, is a legitimate, defensive test, because it uses the program as intended, with a test account, to verify its access controls, rather than trying to circumvent them through unauthorised channels.

Another form of security testing is configuration testing, where a program is run with different configurations, including secure and insecure configurations, to confirm that secure defaults hold, that insecure configurations are clearly warned about, and that production configuration does not accidentally enable debug mode, disable authentication, or expose sensitive data. Those configuration tests, which check a program's behaviour under different settings, catch mistakes where a development configuration leaks into production, or where a default changes unexpectedly after an update, both of which are common security incidents that are preventable through testing. Configuration testing is therefore a valuable, often overlooked, part of security testing.

Dependency and supply chain testing, through auditing tools that check for known vulnerabilities, as discussed earlier, is also part of security testing, because it verifies that a program's trusted components are not known to be vulnerable, at the time of testing. That auditing, run regularly and as part of continuous integration, provides an early warning when a new vulnerability is disclosed in a dependency, allowing it to be addressed promptly, rather than discovered later, after exploitation. That proactive auditing, combined with validation, authorisation, and configuration testing, forms a layered security testing approach, where each layer checks a different aspect of a program's defences, without requiring attack techniques.

Finally, security testing should be documented, with test cases, expected results, and evidence that they were run, so that security claims about a program are supported by evidence, rather than by assumption. A program with documented security tests is a program whose security has been considered explicitly, not merely hoped for, and that explicitness is itself a security practice, because it makes gaps visible and therefore addressable.

Check: How can authorisation be tested defensively without attempting to bypass controls illicitly?

Section 26

Supply chain security concerns the trustworthiness of the code, tools, and services a program depends on, from libraries to build tools to package indexes, and it has become increasingly important as programs depend on more external components. A supply chain attack occurs when an attacker compromises a trusted component, such as by publishing a malicious version of a popular library, or by tampering with a package during distribution, so that programs that install that component unwittingly include malicious code. Defending against such attacks requires verifying the integrity and origin of dependencies, as discussed earlier, by using trusted indexes, by checking package hashes where tools support it, and by being cautious about packages with names similar to popular ones, which may be imitations designed to trick users into installing them by mistake.

Pinning dependencies, meaning recording exact versions that have been tested, is a supply chain practice that improves reproducibility and that can help with security, by ensuring that a program installs the same versions that were tested, rather than newer versions that have not been reviewed, which could include a compromised version released after testing. Pinning alone, however, does not guarantee security, because a pinned version could itself be compromised, or could contain a vulnerability discovered after pinning, which is why pinning should be combined with auditing, as discussed earlier, so that pinned versions are checked against known vulnerabilities, and updated promptly when a vulnerability is found, with testing, as part of routine maintenance.

Verifying the integrity of installed packages, through hashes or signatures where supported, ensures that the package installed matches the package published by its author, and has not been altered in transit or in storage. That verification, which installers often perform automatically for packages from trusted indexes, protects against tampering during distribution, but it does not protect against a compromised author account, where an attacker publishes a malicious version through the legitimate index, with a valid hash, because the hash only proves that the package matches what was published, not that what was published is safe. That limitation means that integrity checking is necessary but not sufficient for supply chain security, and it must be complemented by other practices, such as auditing, reputation checking, and minimising dependencies, to reduce exposure to compromised components.

Minimising dependencies, as discussed earlier, is a supply chain defence, because each dependency is a component that must be trusted, and a program with fewer dependencies has fewer components that could be compromised, and fewer components to audit and update. That minimisation should be balanced against functionality, because reinventing a well-tested library for a complex task, such as cryptography, is riskier than trusting that library, as discussed earlier, but for simple tasks that the standard library can handle, avoiding an extra dependency reduces supply chain risk without sacrificing quality.

Finally, supply chain security includes build tools and development tools, not just runtime libraries, because a compromised build tool could alter a program during building, even if its source code is correct, or a compromised development tool could steal secrets from a developer's environment. Using trusted, well-maintained tools, keeping them updated, and being cautious about installing tools from untrusted sources, are basic supply chain habits for development environments, complementing the practices for runtime dependencies.

Check: Why does pinning a dependency version not, by itself, guarantee supply chain security?

Section 27

Backup security involves protecting copies of data made for recovery, because backups, by design, contain copies of a program's data, including sensitive data, and if those copies are less protected than the original, they become an easier target for attackers seeking that data. A backup stored in plain text, in an accessible location, with broad permissions, exposes the same sensitive data as the original, but with potentially weaker access controls, because backups are often stored on different systems, shared with different people, or retained for longer periods, increasing the window for exposure. Protecting backups, therefore, requires the same care as protecting original data, including encryption, access controls, and retention limits, applied to backup copies as well as to live data.

Encrypting backups is a key protection, especially for backups stored off-site or in shared storage, where physical access or account access by unauthorised people could expose them. Backup encryption should use established methods, with keys managed securely and separately from the backups themselves, so that a backup and its key are not stored together, where both could be obtained at once, defeating encryption's purpose. That key separation, between backup data and backup keys, is similar to secret management generally, and it requires planning, because a backup whose key is lost cannot be restored, while a backup whose key is exposed can be read by anyone who obtains both. Balancing recoverability against confidentiality, through secure key storage with backup, is a practical backup security challenge.

Access controls for backups should limit who can read, restore, or delete them, to authorised administrators only, rather than to all users of a system, because backups often contain data for many users, and a user who can restore a backup could access other users' data, even if they cannot access it through the program's normal authorisation. That access limitation, enforced through file permissions, storage account permissions, and, where applicable, application-level controls, prevents backup access from becoming a bypass for normal authorisation, which is a subtle but important security consideration for programs that handle multi-user data.

Retention and deletion for backups also matter for security and privacy. Backups retained indefinitely accumulate sensitive data, creating a conflict between recovery needs and deletion expectations. A retention policy, stating how long backups are kept, and how deletion requests are handled for backup copies, such as by deleting backups on a schedule, or by ensuring that restored data respects deletion, helps balance recovery against privacy, and it should be documented, so that users and maintainers understand how long their data persists in backups after it is deleted from live systems.

Finally, backup security should be tested, by restoring a backup in a test environment and confirming that it works, that it contains the expected data, and that it does not contain data it should not, such as secrets that were accidentally included, or personal data that should have been excluded. That restore testing, performed periodically, verifies both recoverability, which is backup's primary purpose, and security, by revealing what a backup actually contains, rather than what it is assumed to contain. A backup that has never been restored is, as discussed for other safeguards, only a hope, not a guarantee, and testing it turns that hope into evidence, for both recovery and security.

Check: Why can a backup become an easier target for attackers than the original data?

Section 28

Environment security, in the context of Python development, means protecting the computers, accounts, and tools used to write and run programs, because a compromised development environment can compromise every program built in it, through stolen secrets, altered code, or infected dependencies. A developer's computer should be protected with standard measures, such as keeping its operating system and software up to date, using strong, unique passwords for accounts, enabling multi-factor authentication where available, and being cautious about installing software from untrusted sources, because those measures, while they may seem unrelated to Python programming, form the foundation on which code security rests. A program written on a compromised computer cannot be trusted, regardless of how carefully its code was reviewed, because its build environment itself may have been tampered with.

Account security for developers includes protecting accounts for version control, package indexes, and other services used in development, because those accounts can be used to publish malicious code, to access private repositories, or to steal secrets stored in those services. Using strong, unique passwords, enabling multi-factor authentication, and using scoped tokens, with minimal permissions, for automated tasks, rather than broad passwords, are basic account security habits that protect development accounts from takeover. Those habits, applied to every development account, reduce the risk that a single compromised password grants access to many services, which is a common way that development environments are compromised, through credential reuse across services.

Secrets in development environments, such as keys stored in environment variables, in configuration files, or in a developer's home folder, should be protected with file permissions that restrict access to the developer alone, and should not be shared through insecure channels, such as chat messages, emails, or screenshots, where they could be copied or retained beyond their intended audience. Sharing secrets securely, through a dedicated secret sharing tool or through a secure channel, with an expiry, is a better practice, and it should be used even for temporary sharing, because temporary exposures can become permanent if a secret is copied or retained.

Physical security, while it may seem distant from programming, also matters for development environments, because a computer left unlocked, or a screen displaying sensitive code or secrets, can be accessed by anyone nearby. Locking a computer when stepping away, being aware of who can see a screen when working with sensitive data, and avoiding working with sensitive data in public places where shoulder surfing is possible, are simple physical security habits that complement digital protections, and they are part of a holistic view of environment security, where the weakest link, whether digital or physical, determines overall security.

Finally, environment security should be reviewed periodically, by checking which accounts have access to which services, which secrets are stored where, and which tools are installed, and by revoking access or removing tools that are no longer needed. That periodic review, similar to dependency review, prevents accumulation of stale access, forgotten secrets, and unused tools, each of which increases attack surface over time, even if it was justified when first granted. A development environment that is reviewed and pruned regularly remains secure and manageable, while one that is never reviewed gradually becomes a collection of forgotten access and secrets, waiting to be exploited.

Check: Why does a compromised development environment undermine trust in programs built in it?

Section 29

Command-line tools have specific security considerations, because they accept input from arguments, environment variables, files, and standard input, each of which can be supplied by users or by other programs, and each of which should be validated as discussed generally. Argument handling, in particular, should use a safe parsing library, such as argparse, rather than manual parsing that might mishandle special characters, spaces, or unexpected argument counts, leading to incorrect behaviour or to unintended actions. That safe parsing, combined with validation of argument values, as discussed for input validation, ensures that a command-line tool behaves predictably, even when given unexpected arguments, rather than crashing or performing an unintended action based on a misparsed argument.

Environment variables, as inputs to command-line tools, should also be validated, because they can be set by users, by scripts, or by other programs, and a tool that trusts an environment variable without checking it may be tricked into using a malicious value. Validating environment variables, checking that they are of the expected form and within acceptable limits, and reporting clear errors for invalid values, prevents that trickery, and it is particularly important for variables that affect security, such as those that set file locations, enable debug mode, or specify credentials, where an invalid or malicious value could have serious consequences if trusted blindly.

File arguments to command-line tools, as discussed for file handling generally, should be checked to ensure that they refer to intended locations, that they are readable or writable as needed, and that they are not maliciously crafted to cause the tool to overwrite sensitive files or to read files it should not. That checking, performed before processing, with clear errors for invalid paths, prevents a tool from being used as a vehicle to access files outside its intended scope, whether by a careless user who mistyped a path, or by a malicious user who supplied a crafted path deliberately.

Output from command-line tools should also be handled securely, by avoiding sensitive data in output, and by ensuring that output files are created with restrictive permissions, such as reports with personal data or credentials. A tool that writes a report containing sensitive data to a world-readable file, by default, exposes that data to anyone on the same computer, even if the tool itself is secure, because the output file's permissions determine who can read its contents. Setting restrictive permissions for output files by default, and documenting how to change them if broader access is needed, is a simple output security habit for command-line tools.

Finally, command-line tools that are run by scripts or by schedulers should be designed to fail safely, with clear exit codes and error messages, as discussed earlier, so that a failure does not cause a script to proceed as if the tool had succeeded, potentially acting on incomplete or incorrect output. That fail-safe behaviour, where a tool's failure is visible and its output is not mistaken for success, is a security consideration as well as a reliability one, because a script that proceeds on failed output could take actions based on wrong data, such as deleting files or sending messages, with harmful consequences. Designing command-line tools to fail visibly and safely, with truthful exit codes, is therefore part of their security, not just their usability.

Check: Why should environment variables used by a command-line tool be validated like other inputs?

Section 30

Security culture, for a solo developer or a team, means treating security as a normal part of development, not as an extra task to be done only when a problem occurs, or only by specialists. That culture starts with habits, such as validating input, protecting secrets, using safe defaults, and reviewing code for security, applied consistently to every change rather than only to features that are obviously security-related. Those habits, practised daily, become automatic, reducing the effort needed to write secure code, and reducing the chance that a security issue slips through because it was not considered relevant to a particular change, when in fact every change that accepts input, handles data, or performs an action has security implications.

Learning about security should be continuous, because threats, tools, and best practices evolve, and a developer who stops learning will gradually fall behind, using outdated practices that may no longer be sufficient. That continuous learning, integrated into routine development, such as by reading a security note when updating a dependency, keeps security knowledge current without requiring dedicated study time that may be hard to find.

Sharing security knowledge, within a team or with the wider community, through code review comments, documentation, or informal discussion, helps build a culture where security is everyone's responsibility, not just the responsibility of a designated security person, who may not exist in a small team. When a developer explains, in a review, why a particular pattern is risky and suggests a safer alternative, they teach their colleagues, who will then apply that lesson to their own code, multiplying the benefit of a single review.

Security culture also involves admitting mistakes and learning from them, rather than hiding them out of embarrassment, because security incidents, like bugs generally, are inevitable, and the value of an incident lies in what is learned from it, not in whether it occurred. A developer or team that openly discusses a security mistake, analyses its root cause, and updates practices to prevent recurrence, improves over time, while one that hides mistakes, or blames individuals, learns nothing and repeats the same mistakes, with increasing consequences. That openness, combined with a focus on systemic fixes rather than individual blame, is a hallmark of a healthy security culture, and it applies to solo developers as well, who can learn from their own mistakes by recording them and updating their personal checklists.

Finally, security culture should be proportionate, balancing security against usability, performance, and development speed, rather than pursuing perfect security at the expense of all else, which is unattainable and which can lead to security measures that are bypassed because they are too cumbersome to use. A proportionate culture asks, for each program, what data it handles, what threats it faces, and what protections are reasonable for its context, and it applies those protections consistently, without over-engineering for low-risk programs, or under-protecting high-risk ones. That proportionality, guided by threat modelling, as discussed at the start of this chapter, ensures that security effort is spent where it matters most, making secure development sustainable, rather than a burden that is abandoned under pressure.

Check: Why should security be treated as a normal part of development rather than a specialist task?

Section 31

Designing a secure application programming interface, or API, starts with authentication and authorisation for every request, as discussed generally, but with API-specific details that matter for remote access. An API should require authentication for any action that is not explicitly public, using tokens or keys that are scoped, as discussed for service keys, and it should check authorisation for the specific resource requested, not just for the API as a whole, because a user who is authorised to view one item may not be authorised to view another, even if both are accessed through the same endpoint. That per-resource authorisation check, performed for every request, prevents a common API problem where an authenticated user can access another user's data simply by changing an identifier in a request, which is a serious failure that should be tested for explicitly, as discussed for authorisation testing.

Rate limiting is another API security practice, where an API limits how many requests a client may make in a given period, to prevent abuse, such as brute-force guessing of identifiers, or overwhelming the service with requests, as discussed for denial of service. Rate limits should be set generously enough for legitimate use, with clear error responses when a limit is exceeded, so that legitimate clients can adjust their behaviour, such as by slowing down or by waiting, rather than being blocked without explanation.

API responses should also be minimised, returning only the data a client needs for its purpose, rather than full internal records that may contain sensitive fields, such as internal identifiers, passwords hashes, or other users' data, which a client could misuse or leak. That minimisation, similar to data minimisation generally, requires reviewing API output fields deliberately, rather than returning an internal object directly, because an internal object may contain fields that were not intended for clients, and that may be exposed accidentally when the object's structure changes. Returning a deliberately constructed response, with only intended fields, prevents that accidental exposure, and it makes API behaviour stable, even as internal data structures evolve.

Versioning and deprecation for APIs, as discussed for packages generally, also have security implications, because an old API version may lack security protections added to a newer version, such as stronger authentication or better validation, and keeping old versions available indefinitely prolongs exposure to those older, weaker protections. Deprecating old API versions, as discussed for feature deprecation, helps users move to safer versions, while giving them time to update. That deprecation process, communicated clearly to API users, balances security against compatibility, by not breaking users abruptly, while not leaving insecure versions available forever.

Finally, API documentation should include security guidance, explaining how to authenticate, how to handle tokens securely, what rate limits apply, and what data is returned, so that API users can integrate securely, without needing to guess or to experiment with insecure practices. That documentation, written for developers using the API, is part of API security, because an API that is hard to use securely will be used insecurely, even by well-intentioned developers, who may store keys in code, ignore rate limits, or log sensitive responses, simply because secure use was not explained. Clear, secure-by-default documentation, with examples that show secure practices, helps API users do the right thing, which is ultimately what API security aims to achieve.

Check: Why should API authorisation be checked per resource, not just per API as a whole?

Section 32

When a data breach or a security incident affects users, communication with those users is a critical part of response, as discussed briefly earlier, and it deserves deeper attention because it affects trust, legal obligations, and users' ability to protect themselves. Communication should be prompt, once the basic facts are known, even if the full investigation is not complete, because delaying communication while waiting for perfect information leaves users unaware that they may need to act, such as by changing passwords, while an attacker may already be using exposed data.

Communication should be clear and specific, naming what data was involved, in general terms, without revealing sensitive details that could help an attacker, and explaining what users should do, in concrete steps, such as changing a password, enabling multi-factor authentication, or watching for suspicious activity. Vague communication, such as saying that an incident occurred without saying what data was affected or what users should do, leaves users anxious and unable to act, while overly technical communication, full of jargon, may confuse users who are not technical, preventing them from taking the steps that would protect them. Balancing specificity with clarity, by using plain language and concrete actions, is the goal for breach communication, and it should be reviewed for clarity by someone who is not a security expert, to ensure that it is understandable to its intended audience.

Communication channels should be trustworthy, so that users can verify that a message about a breach is genuine, not a phishing attempt exploiting the breach to trick users into revealing passwords or installing malicious software. That trustworthiness can be achieved by communicating through established channels, such as a program's official website, its verified email, or its in-application notification system, rather than through unexpected channels, and by advising users to verify messages by visiting the official site directly, rather than by clicking links in an email, which could be spoofed.

Legal and regulatory obligations for breach communication vary by jurisdiction and by the type of data involved, and they may require notification within a specific timeframe, to specific authorities, or to affected users, with specific content. Those obligations, while beyond the scope of this chapter's technical focus, should be understood by anyone responsible for a program that handles personal data, through appropriate legal guidance, because failing to meet them can have serious consequences beyond the breach itself, including fines and loss of trust. That legal awareness, combined with technical response, forms a complete incident response, where both technical and legal aspects are handled, rather than only one.

Finally, after breach communication, the program's author should follow up, with updates as the investigation progresses, with confirmation when the problem is fixed, and with an explanation of what has changed to prevent recurrence, in general terms that do not reveal exploitable details. That follow-up closes the communication loop, reassures users that the problem was taken seriously, and demonstrates accountability, which helps rebuild trust after a breach, even though trust may take time to fully recover. A breach handled with prompt, clear, trustworthy communication and follow-up is far less damaging to trust than one handled with delay, vagueness, or silence, even if the technical facts of the breach are similar, because users judge response as well as incident.

Check: Why is prompt initial breach communication better than waiting for perfect information?

Section 33

Reviewing contributions from others, such as code changes submitted to an open source project, involves security considerations beyond ordinary code review, because contributed code may come from someone unknown, and it may contain, deliberately or accidentally, security issues that the project's maintainers must catch before merging. That review should apply the same security checklist as for one's own code, with additional attention to new dependencies, new inputs, new file handling, and new authentication or authorisation logic, because those are the areas where a contribution is most likely to introduce a vulnerability, whether through inexperience or through intent.

New dependencies in a contribution deserve particular scrutiny, because a dependency added by a contributor may be unnecessary, unmaintained, or malicious, even if its name looks legitimate, as discussed for supply chain security. A reviewer should ask whether the dependency is needed, whether the standard library or an existing dependency can do the job, whether the dependency is well-maintained and widely used, and whether its licence is compatible with the project. Those questions, asked for every new dependency, prevent a project's dependency list from growing with unreviewed, risky components, and they ensure that each dependency earns its place, as discussed earlier for dependency choice.

Contributed code that handles untrusted input, performs file operations, or changes authentication or authorisation should be reviewed with extra care, by tracing data flow, checking validation, and testing with invalid inputs, as discussed for security review generally. That care is especially important for contributions from new contributors, who may not yet be familiar with a project's security practices, and who may write code that works functionally but that handles input unsafely, such as by constructing queries or paths by hand, rather than using safe libraries. Reviewing such code kindly, with explanations and suggestions for safe alternatives, as discussed for security review culture, helps new contributors learn secure practices, while still protecting the project, which is a better outcome than either merging unsafe code or rejecting a contribution without explanation.

Automated checks, such as secret scanning, dependency auditing, and test suites including security-relevant tests, should be run on contributions before merging, to catch common issues that human review might miss, such as a hard-coded secret, a known vulnerable dependency, or a failing validation test. Those automated checks, integrated into a contribution workflow, provide a consistent safety net, regardless of who reviews a contribution, and they free human reviewers to focus on design and logic, rather than on scanning for known patterns.

Finally, contributions should not be merged based solely on trust in the contributor, even if the contributor is well-known, because even experienced developers make security mistakes, and because accounts can be compromised, allowing an attacker to submit code under a trusted name. That principle, of reviewing every change on its merits, regardless of author, is a basic security practice for collaborative projects, and it should be applied consistently, without exception for senior contributors, because exceptions create a path by which a compromised account or a careless change can bypass review. Consistent review, for every contribution, is what makes open collaboration safe, by ensuring that trust is placed in the review process, not in any individual contributor alone.

Check: Why should every contribution be reviewed on its merits, even from a well-known contributor?

Section 34

Handling untrusted code, such as plugins, extensions, or user-supplied scripts that a program may load and run, is a high-risk design decision that should be approached with extreme caution, because running untrusted code with a program's permissions effectively grants that code all of the program's capabilities, including access to its data, its files, and its network. That risk means that, in most cases, a program should not run untrusted code at all, and should instead offer a safer alternative, such as a configuration file, a data format, or a limited set of options, that allows users to customise behaviour without executing arbitrary code.

If a program must support plugins or extensions, it should do so through a carefully designed plugin interface, with limited capabilities, clear documentation, and, where possible, isolation, such as running plugins in a separate process with restricted permissions, rather than in the program's own process with full access. That isolation, while it adds complexity, contains the damage if a plugin is malicious or buggy, because the plugin's process can only access what it is explicitly granted, rather than everything the main program can access. Designing such isolation requires thought about what plugins need, such as which data they may read, which actions they may perform, and how they communicate results back to the main program, and it should be done before plugins are supported, not added as an afterthought after plugins are already running with full access.

Evaluating code, such as using Python's evaluation functions on user-supplied text, is a specific form of running untrusted code, and it should be avoided for untrusted input, because evaluation treats text as code, allowing any operation the program can perform, with the program's permissions, if the text is crafted to do so. That risk applies even if evaluation is restricted in apparent ways, such as by limiting available names, because restrictions on evaluation are notoriously difficult to make secure, and bypasses are regularly discovered for supposedly safe evaluation schemes. The defensive rule is simple: do not evaluate untrusted text as code, and use a data parser, such as JSON, for untrusted data, as discussed for deserialisation, rather than an evaluator, even if evaluation seems convenient for a particular feature.

Similarly, running external programs or shell commands with user-supplied input, as discussed for subprocess handling in earlier chapters, should be done with safe argument passing, as a list rather than as a shell string, and with validation of input, to prevent user data from changing the command's meaning. That safe subprocess handling, combined with least privilege, by running external programs with minimal permissions, and with clear limits on what they can do, reduces the risk of command execution becoming a security problem, even when external programs are necessary for a program's function. Those subprocess practices, while they were introduced for automation, are also security practices, because they prevent untrusted input from becoming a command.

Finally, when a program does run code or commands from an external source, it should log what was run so that unexpected or malicious actions can be detected and investigated. The safest plugin is one that does not run arbitrary code at all, and that principle should guide design whenever customisation is considered.

Check: Why is running untrusted code with a program's full permissions so risky?

Section 35

Security metrics and auditing involve measuring a program's security posture over time, through counts and checks that reveal whether protections are working, whether vulnerabilities are being addressed, and whether security practices are being followed consistently. Metrics might include the number of known vulnerabilities in dependencies, and how long they remain unfixed, the number of secrets found in code scans, the time taken to respond to security reports, and the coverage of security-relevant tests, such as validation and authorisation tests. Those metrics, tracked over time, show trends, such as whether vulnerabilities are being fixed faster or slower, and whether security practices are improving or deteriorating, which is more useful than a single snapshot, because it reveals direction, not just current state.

Auditing, in this context, means systematically checking a program and its environment against a checklist, such as the security checklist discussed earlier, to confirm that protections are in place and working, rather than assuming they are. An audit might check that secrets are not in code, that dependencies are up to date, that file permissions are restrictive, that authentication is enforced, and that logs do not contain sensitive data, using a combination of automated tools and manual review, as discussed for security review.

Metrics should be interpreted carefully, because a metric that is optimised in isolation can be misleading. For example, a program with zero known vulnerabilities in its dependencies might still be insecure, if its own code has validation flaws, or if its dependencies are outdated but their vulnerabilities have not yet been disclosed, while a program with several known, low-severity vulnerabilities that are being addressed promptly might be more secure in practice, because its maintainers are actively managing risk. Metrics are therefore indicators, not guarantees, and they should be combined with qualitative judgement, from code review, threat modelling, and incident history, to form a complete picture of security posture, rather than being treated as a score that proves security.

Auditing should also include the development environment and the deployment environment, not just code, because a secure program deployed in an insecure environment, with broad permissions, outdated tools, or exposed secrets, is still at risk, as discussed for environment security. An audit that checks deployment configuration, such as whether debug mode is disabled in production, whether backups are encrypted, and whether access to production systems is limited to authorised administrators, complements code auditing, by ensuring that the environment in which code runs is as secure as the code itself. That holistic audit, covering code, dependencies, configuration, and environment, is more valuable than a narrow audit that checks only one aspect, because security failures can arise from any aspect, and a holistic audit is more likely to catch them.

Finally, metrics and audit results should be acted upon, not merely collected, because a metric that is measured but ignored, or an audit finding that is recorded but not fixed, provides no security benefit, and it may create a false sense of security, where a program appears to be monitored, while its known problems remain unaddressed. Acting on findings, by prioritising them based on severity and impact, fixing them promptly, and verifying the fix, turns measurement and auditing from a bureaucratic exercise into a practical security improvement process, which is their purpose.

Check: Why are security metrics indicators rather than guarantees of security?

Section 36

Secure deletion and data lifecycle management involve handling data from creation to deletion, with security in mind at each stage, because data that is no longer needed but that remains stored, in files, backups, logs, or caches, continues to be at risk, even though it is no longer useful. A data lifecycle plan, stating what data is collected, how long it is kept, where it is stored, including backups and logs, and how it is deleted when no longer needed, provides a framework for managing that risk, by ensuring that data is not kept indefinitely by default, but is deleted on a schedule, or when its purpose is fulfilled, as discussed for data minimisation generally. That plan, documented and followed, turns deletion from an afterthought into a routine part of data handling, which is essential for both security and privacy.

Deleting data securely, in the sense of making it unrecoverable, is more complex than simply removing a file or a database record, because storage systems may retain copies, such as in backups, in file system journals, or in unallocated space that has not yet been overwritten. For most programs, the practical steps for secure deletion are to delete the primary copy promptly, to ensure that backups containing the data are also deleted on their retention schedule, rather than kept indefinitely, and to avoid unnecessary copies, such as in logs or in temporary files, that would need separate deletion. Those steps, while they do not guarantee that every physical copy is overwritten, which may require specialised tools or storage features, significantly reduce the risk that deleted data remains accessible through ordinary means, such as restoring a backup or reading a log file.

Data lifecycle management also includes handling data subject requests, such as requests to delete personal data, or to correct inaccurate data, where applicable, by ensuring that deletion or correction propagates to all copies, including backups and caches, within a reasonable time, rather than only to the primary copy, leaving other copies untouched. That propagation, while it can be technically challenging, especially for backups, is part of responsible data handling, and it should be planned for, by designing data storage so that copies are tracked and can be located, rather than scattered across many files and systems without an inventory.

Caching and temporary data also have lifecycles that should be managed securely. A cache that stores sensitive data should expire entries after a reasonable period, and should clear entries when they are no longer needed, rather than retaining them indefinitely, because a cache that is never cleared becomes a long-term store of sensitive data, with potentially weaker protections than the primary store. Similarly, temporary files containing sensitive data should be deleted promptly after use, as discussed for file handling, rather than left in a temporary folder where they may accumulate and be forgotten, increasing exposure over time.

Finally, data lifecycle management should be reviewed periodically, by checking retention policies against actual practice, by verifying that deletion schedules are being followed, and by updating policies as a program's data collection changes, such as when new features add new data types, or when old features are removed and their data is no longer needed.

Check: Why is deleting a primary copy of data not sufficient for secure deletion?

Section 37

Local tools and command-line programs have authentication considerations that differ from web programs, because they often run on a single user's computer, with that user's permissions, and they may not need a separate login, since the operating system already authenticates the user. That operating system authentication, however, should not be assumed to be sufficient for all purposes, especially for tools that access sensitive data, perform destructive actions, or communicate with external services, where additional authentication, such as a service key or a user confirmation, may be needed to ensure that the action is authorised, not just that the computer's user is logged in.

For local tools that store credentials, such as service keys for external APIs, secure storage is important, because a key stored in plain text in a configuration file, in a user's home folder, is accessible to any program running as that user, including malicious programs, and it may be copied or backed up inadvertently, as discussed for secrets generally. Using the operating system's secure credential store, where available, which encrypts credentials and restricts access to the owning user and, where possible, to the specific program, is a safer storage method than a plain text file, though it requires platform-specific code or libraries, and it may not be available in all environments.

Local tools that perform destructive actions, such as deleting files or modifying data, should require explicit confirmation for those actions, especially when the action is irreversible, or when it affects many files, as discussed for command-line tools generally. That confirmation, through a prompt or a flag, ensures that a user does not accidentally trigger a destructive action through a mistyped command, or through a script that calls the tool with wrong arguments, and it provides a moment for the user to reconsider, which is a simple but effective authorisation check for local actions. That confirmation should be bypassable only through an explicit flag, for scripted use, where the user has deliberately accepted the risk, as discussed for force flags, rather than being disabled by default, which would remove its protection for interactive users who need it most.

Local tools should also be careful about output and logs, as discussed generally, because a tool run on a shared computer, or whose output is captured in a script's log, may expose sensitive data to other users or to log files that are less protected than the tool's own data. Avoiding sensitive data in output, setting restrictive permissions for output files, and warning users when output contains sensitive data, so that they can protect it appropriately, are local tool security habits that complement the general logging and file permission practices discussed earlier.

Finally, local tools should be updated and maintained like any other program, with dependencies audited, vulnerabilities fixed, and versions documented, because a local tool with an outdated, vulnerable dependency can be exploited by a malicious file or input, even if it is not exposed to a network, through crafted files or arguments that trigger a vulnerability in its parsing or processing code.

Check: Why might a local tool need additional confirmation for destructive actions even though the operating system already authenticated the user?

Section 38

Deployment security involves configuring the environment where a program runs, in production or in shared use, so that it is protected against unauthorised access, tampering, and information disclosure, and it starts with running the program with minimal permissions, as discussed for least privilege, rather than as an administrator or with broad system access. A program deployed as an administrator can, if compromised, affect the entire system, while one deployed as a limited user, with access only to its own files and to the services it needs, limits damage to its own scope, which is a fundamental deployment protection that should be applied regardless of the program's own code security, because code security cannot be perfect, and deployment limits provide a second layer of defence when code fails.

Network configuration for deployment should also follow least privilege, by exposing only the network ports and services that a program genuinely needs, and by restricting access to those services to authorised clients, through firewalls, access controls, or private networks, rather than exposing a program to the entire internet by default, when it is only intended for local or internal use. Checking deployment network configuration, before a program is made available, is a simple but critical deployment security step.

Environment variables and configuration for deployment should be set securely, with secrets stored in a secure source, as discussed generally, and with debug mode, verbose logging, and other development settings disabled, because development settings often expose internal details, or disable protections, for convenience during development, and they should never be enabled in production, where they could leak information or weaken security. That development-versus-production configuration separation, enforced through environment-specific configuration files or through deployment checks that verify that debug mode is off, prevents a development shortcut from becoming a production vulnerability, which is a common deployment mistake, as discussed for framework defaults.

Keeping deployment dependencies up to date, including the operating system, the Python interpreter, and system libraries, as well as Python packages, is also part of deployment security, because vulnerabilities in any layer, from the operating system to the application, can be exploited to compromise a deployed program, even if the program's own code is secure. That system-level updating, performed regularly and tested, as with application updates, ensures that a deployed program benefits from security fixes at every layer, rather than only at the application layer, and it should be planned as part of deployment maintenance, not left to chance, because an outdated system component can undermine otherwise sound application security.

Finally, deployment should include monitoring and logging, with logs protected and reviewed, as discussed for logging generally, so that suspicious activity, such as repeated failed access attempts, unusual traffic patterns, or errors indicating an attack, can be detected and investigated promptly, rather than going unnoticed until damage is done. That monitoring, combined with a basic incident response plan, as discussed earlier, turns deployment from a one-time setup into an ongoing security practice, where a deployed program is watched, maintained, and updated, rather than installed and forgotten, which is essential for long-term security, because threats evolve, and a deployment that was secure when first set up may become insecure over time, as new vulnerabilities are discovered and as its environment changes.

Check: Why should a program intended for internal use not be exposed to the entire internet by default?

Section 39

Teaching security to others, whether to colleagues, to learners, or to users, is an important part of defensive security, because a program's security depends not only on its code, but also on how its users and maintainers understand and use it. Teaching should focus on practical habits, such as validating input, protecting secrets, using safe defaults, and checking authorisation, illustrated with concrete examples from real code, rather than on abstract theory alone, because habits are learned through practice and through seeing how they apply to familiar tasks, such as writing a command-line tool or handling a file, rather than through memorising security terminology. That practical, example-based teaching, as used throughout this chapter, helps learners internalise security as part of normal programming, rather than as a separate, intimidating subject.

Onboarding new developers to a project's security practices should include a brief introduction to the project's threat model, its sensitive data, its authentication and authorisation rules, and its security checklist, so that a new contributor understands, from the start, what is sensitive, what protections exist, and what is expected of their code. That onboarding, which can be as simple as a short document and a walkthrough of a recent security review, prevents new contributors from inadvertently introducing vulnerabilities through ignorance of a project's practices, and it helps them contribute securely from their first change, rather than learning through rejected changes or through incidents.

Teaching users about security, for programs that handle their data, should focus on actions they can take, such as choosing strong passwords, enabling multi-factor authentication where available, being cautious about phishing messages, and reporting suspicious activity, rather than on technical details they cannot control, such as internal code practices. That user-focused teaching, delivered through clear, concise guidance in documentation, in onboarding messages, or in help text, empowers users to protect themselves, without overwhelming them with information they cannot act on, and it complements technical protections, by addressing the human element of security, which is often the weakest link, not because users are careless, but because they lack clear guidance on what to do.

Security teaching should also be honest about limitations, acknowledging that no program is perfectly secure, and that security is about managing risk, not eliminating it entirely, because overpromising security, by claiming that a program is unbreakable or that data is completely safe, creates false confidence that can lead users to take risks they would otherwise avoid, such as sharing sensitive data more freely, or neglecting their own protective actions. Honest teaching, which explains what protections exist, what risks remain, and what users and developers can do to reduce those risks, builds realistic trust, based on understanding, rather than false trust based on overpromising, which is fragile and which collapses when an incident inevitably occurs, even a minor one.

Finally, teaching security should be ongoing, not a one-time lesson, because threats evolve, practices change, and new developers and users join a project over time, each needing to learn its security practices. Regular, brief updates, such as notes about a new dependency vulnerability and how it was addressed, or about a security practice that was improved after an incident, keep security awareness current, without requiring extensive training sessions, and they demonstrate that security is a living practice, not a static set of rules learned once and forgotten.

Check: Why should security onboarding explain a project's threat model and sensitive data to new contributors?

Section 40

This chapter has covered defensive security for Python developers, from threat modelling and input validation, through secrets, authentication, data handling, and file security, to dependencies, deployment, and incident response, and its lessons can be summarised as a set of habits that, practised consistently, make programs significantly more secure, even without specialist security expertise. Those habits include validating all untrusted input at trust boundaries, using allowlists, keeping secrets out of code and logs, using established libraries for sensitive operations, choosing safe defaults, checking authorisation for every sensitive action, applying least privilege, keeping dependencies updated, and documenting security-relevant decisions, so that they can be reviewed and maintained over time. That list, while long, is not a set of isolated rules, but a coherent approach, based on defence in depth, where multiple layers together provide stronger protection than any single layer alone.

The central theme of this chapter is that security is not an add-on, but a part of good programming, integrated into design, coding, testing, and maintenance, rather than applied at the end, when vulnerabilities are harder and more expensive to fix. A program designed with security in mind, with clear trust boundaries, minimal data collection, safe defaults, and explicit authorisation checks, is easier to make secure than one where security is retrofitted, because its structure already supports secure behaviour, rather than working against it. That design-first approach, applied from the first sketch of a program, through its implementation, to its deployment and maintenance, is the most effective way to achieve security, and it is also the most efficient, because it avoids costly rework and incident response later.

Looking ahead, security skills will interact with many topics in later chapters, including performance, where security measures such as encryption and hashing have costs that must be balanced against speed, and clean code, where clear, simple code is easier to review for security than complex, tangled code. Those interactions show that security is not isolated; it is one aspect of writing good software, alongside correctness, performance, maintainability, and usability, and it must be balanced against those other aspects, through judgement, measurement, and threat modelling, rather than pursued in isolation at the expense of all else. That balanced view, where security is important but not the only consideration, leads to programs that are both secure and usable, which is the goal, because a secure program that nobody can use, or that is too slow to be useful, has failed in a different way, even if it has not been breached.

As with previous chapters, the best consolidation is practice with a real program, applying the security checklist from this chapter to its code, its configuration, and its deployment, and fixing any issues found, however small. That practice, performed on a program you know well, will reveal security considerations that reading alone cannot, such as a secret in a log statement you had forgotten, a file path that is not validated, or a dependency that is outdated, and fixing those issues will teach you more than any further reading, because it will connect security principles to concrete code you understand. Keep the checklist, update it with lessons from practice and from incidents, and use it for every future program, so that defensive security becomes a habit, not a hurdle, in your development work.

Check: Why is security easier to achieve when considered during design rather than retrofitted at the end?

Section 41

Programs that analyse data, such as summarising records, generating reports, or visualising trends, face security considerations that are easy to overlook, because analysis feels like a read-only, harmless activity. An analysis program that reads files from untrusted sources, however, must validate those files, as discussed for file handling, because a malformed file can crash the program, consume excessive memory, or, if the file format is complex, trigger a vulnerability in a parsing library. That validation, with size limits and format checks, before analysis begins, prevents an analysis job from becoming a denial of service, or from producing incorrect results based on malformed data that was assumed to be clean, simply because it came from a data file rather than from a user typing input.

Analysis output also needs care, because a report or visualisation that aggregates personal data may still reveal individuals, if the aggregation is too fine-grained. That re-identification risk, discussed for anonymisation, means that analysis output should be reviewed for privacy, not just for accuracy, before it is shared, especially when it will be shared publicly or with a broad audience. Aggregating to larger groups, suppressing small counts, or adding noise, where appropriate and where the analysis method supports it, are techniques that can reduce re-identification risk, though they should be applied with understanding of their effect on accuracy, rather than blindly, because over-aggregation can make a report useless, while under-aggregation can expose individuals.

Analysis programs often run with access to large datasets, including sensitive data, and they should follow least privilege, by accessing only the data needed for a particular analysis, rather than all available data, and by storing results with appropriate permissions, as discussed for data handling generally. A program that copies a full sensitive dataset into a working folder, to analyse a small subset, creates an unnecessary copy that must then be protected and deleted, increasing risk for no benefit, while a program that queries only the needed subset, and deletes its working files promptly, minimises exposure. That minimisation, applied to analysis workflows, is a practical security habit that also improves efficiency, by reducing data movement and storage.

Sharing analysis code, as opposed to analysis output, also has security implications, because code may contain hard-coded paths, credentials, or references to internal systems, even if its output is clean. Reviewing analysis code before sharing it, for secrets, for internal details, and for assumptions about data locations, prevents accidental disclosure through code, which is a common way that sensitive information leaks, because code is often shared more casually than data, with less review. That code review, using the security checklist, should be part of sharing any analysis program, whether with colleagues, in a public repository, or as part of a report's supplementary material, because code, like data, can reveal information about its environment.

Finally, analysis programs should be reproducible, in the sense that their results can be regenerated from their inputs and code, with documented versions and settings, because reproducibility helps with both accuracy and security: if a result is questioned, it can be regenerated and checked, and if a data source changes, the effect on results can be assessed, rather than guessed.

Check: Why might an aggregated analysis report still reveal an individual person?

Section 42

Automation scripts, as covered in an earlier chapter, have security considerations that arise from their unattended nature, their file operations, and their often broad permissions. A script that runs on a schedule, without a person watching, must be particularly careful about input validation, because it cannot ask a user to confirm an unexpected input, and it must handle unexpected files, missing files, and malformed data gracefully, with clear logs, rather than proceeding on bad data or failing silently.

Automation scripts often run with permissions that allow them to read and write many files, and they should follow least privilege, by running as a user with access only to the files they need, rather than as an administrator, and by operating only within intended folders, with path validation, as discussed for file handling. A script that runs as an administrator and that accepts a file path from an external source, such as a folder watched for new files, could be tricked into operating on sensitive files outside its intended scope, if its path validation is weak, whereas a script running as a limited user, with path validation, would be contained, even if its validation failed, because its permissions would prevent access to sensitive locations. That layered protection, of limited permissions plus validation, is defence in depth for automation.

Scripts that send messages, generate reports, or trigger other actions should also be careful about output, because an automation script that generates a report from untrusted data, and then sends that report to users, could inadvertently include sensitive data from its input, or could be manipulated to send a misleading report, if its input is not validated and its output is not reviewed. That output review, for scheduled reports, can be as simple as logging a summary of what was sent, with counts and dates, so that a human can spot an unusual report, or as structured as requiring human approval for sensitive actions, as discussed for approval workflows in other contexts.

Automation scripts should also be protected from tampering, because a script that runs unattended, from a known location, could be modified by an unauthorised user with write access to that location, causing it to perform different actions on its next run, without its owner realising. Protecting script files with restrictive permissions, so that only authorised users can modify them, and checking script integrity, where feasible, such as by verifying that a script has not changed unexpectedly, are basic automation security habits.

Finally, automation scripts should be logged and monitored, as discussed for automation generally, with logs that record what was done, what was skipped, and what failed, without recording secrets or sensitive data, so that unusual automation behaviour can be detected, such as a script suddenly processing many more files than usual, or accessing files it normally does not, which could indicate tampering or a change in input that should be investigated. That monitoring, combined with limited permissions, validation, and file protection, makes automation both efficient and defensible, by ensuring that its benefits, of speed and consistency, are not achieved at the cost of security, through unattended, unmonitored, broadly privileged operation.

Check: Why is unattended automation particularly sensitive to unvalidated input?

Section 43

Client programs that call external APIs, as opposed to servers that provide APIs, have their own security considerations, focused on handling responses safely, protecting credentials, and respecting the service's rules. A client should validate API responses generally, checking that response data is of the expected form, within expected size limits, and safe to use, before acting on it, because a service can return unexpected data due to a bug, a change, or a malicious intermediary, if the connection is not properly secured, and acting on that data without validation could cause a client to display incorrect information, write incorrect data, or perform unintended actions.

Client credentials, as discussed for service keys, should be stored securely, scoped narrowly, and never logged, and clients should also be careful about where credentials are sent, by verifying that they are only sent to the intended service, over an encrypted connection, with the service's identity verified, rather than to any server that claims to be that service, or over an unencrypted connection where they could be intercepted. That verification, performed by the HTTP library or framework used, through certificate checking, should not be disabled, even for testing, without understanding the risk, because disabling certificate verification for convenience, and then forgetting to re-enable it, is a common client security mistake that exposes credentials and data to interception, as discussed for framework defaults generally.

Clients should also respect rate limits and usage rules set by a service, not only for politeness, but for security, because a client that sends requests too rapidly, or that retries aggressively on failure, can overwhelm a service, causing it to reject legitimate requests, or can be mistaken for an attack, leading to its key being revoked, which disrupts its own operation and potentially that of other users. Pacing requests, backing off on errors, and honouring rate limit responses, are therefore both good citizenship and good security, because they prevent a client from becoming, inadvertently, a source of denial of service, and they preserve the client's access to the service over time.

Client error handling should avoid leaking credentials or sensitive data, as discussed for error handling generally, by logging generic errors rather than full request and response data, which may contain tokens, personal data, or internal service details. That generic logging, combined with detailed logging in a secure, local environment for debugging, where appropriate, balances diagnosability against security, by allowing developers to investigate failures without creating a persistent record of sensitive data in logs that may be shared or retained longer than necessary. That balance, applied to client errors as to other errors, is part of defensive client design.

Finally, clients should be updated regularly, with their libraries and dependencies audited, as discussed generally, because client libraries, especially HTTP libraries, receive security fixes for issues such as certificate verification, redirect handling, and parsing, which, if not updated, could leave a client vulnerable to interception or to malformed responses, even if its own code is correct. That updating, combined with response validation, secure credential handling, rate limit respect, and safe error logging, forms a layered defence for API clients, ensuring that they can use external services safely, without becoming a weak link in a larger system's security, through neglected client-side protections.

Check: Why should an API client validate responses even from a trusted service?

Section 44

Security documentation, for a program, should explain its security-relevant behaviour, its assumptions, and its limitations, in a way that helps users and maintainers use it safely, without revealing exploitable details that would help an attacker. That documentation might include a security section in a readme, explaining what data a program handles, what protections it provides, such as encryption or authentication, what configuration is required for secure use, and what known limitations exist, such as data that is not encrypted, or features that should not be used with untrusted input.

Security documentation should also include guidance for secure configuration, with examples showing secure values, as discussed for configuration documentation generally, rather than insecure examples copied for convenience, because users often copy examples verbatim, and an insecure example can lead to insecure deployments, even if the program itself is capable of secure operation.

Documentation for reporting security problems is another important element, often called a security policy, which tells users and researchers how to report a suspected vulnerability, what information to include, and what to expect in response, such as acknowledgement within a certain time, and how the report will be handled, including whether the reporter will be credited, and whether the vulnerability will be disclosed publicly after a fix, and when. That policy, even for a small project, encourages responsible reporting, by giving reporters a clear, safe channel, rather than leaving them to report publicly, or to not report at all, for fear of legal or social consequences, and it helps maintainers receive reports in a useful form, with enough detail to reproduce and fix the problem, rather than as vague warnings that cannot be acted upon.

Security documentation should be kept up to date, as a program evolves, because a document that describes protections that no longer exist, or that omits new protections, misleads users and maintainers, potentially causing them to rely on a protection that has been removed, or to overlook a new protection that should be used. Updating security documentation as part of a change, just as with other documentation, as discussed for packaging and for command-line tools, ensures that it remains accurate, and it should be reviewed as part of code review for security-relevant changes, to confirm that documentation reflects the change, including any new risks or mitigations it introduces. That documentation maintenance, while it requires effort, is part of security maintenance, because inaccurate documentation is worse than no documentation, as it actively misleads.

Finally, security documentation should be accessible to its intended audience, written in plain language, organised clearly, and linked from the program's main documentation, so that users who need it can find it easily, rather than it being buried in a separate file that nobody reads, or written in jargon that only security experts understand. That accessibility, combined with accuracy and completeness for essentials, makes security documentation genuinely useful, rather than a formal document that satisfies a checklist but that nobody uses, and it supports a security culture, as discussed earlier, where security information is shared openly and clearly, rather than hidden or obscured, which ultimately makes programs safer, because their users and maintainers understand how to use and maintain them securely.

Check: Why should security documentation describe limitations as well as protections?

Section 45

Fuzzing, in a defensive testing sense, means automatically feeding a program many randomly generated, invalid, or boundary-value inputs, and checking that it handles them safely, without crashing in an unsafe way, without producing incorrect output that is presented as correct, and without leaking sensitive information in errors. That testing, which uses generated inputs, not attack payloads designed to exploit a specific vulnerability, is a legitimate, standard technique for finding robustness bugs, such as crashes on malformed files, hangs on large inputs, or errors on unexpected types, which, while they may not be security vulnerabilities in themselves, can indicate validation weaknesses that could become security issues if exploited, or that could cause denial of service through crashes or resource exhaustion. Fuzzing, therefore, is a defensive testing tool, not an attack tool, when used on one's own program, with one's own test data, to improve its robustness.

Fuzzing should be applied to input handling code, such as parsers for file formats, command-line argument handling, and network request handling, where unexpected input is most likely to cause problems, because those components are designed to accept data from external sources, and they must handle any data, not just well-formed data, safely. A fuzzer generates inputs by mutating valid inputs, such as changing bytes, truncating files, or inserting unexpected values, and by generating completely random inputs, and it runs the program with each input, recording any crashes, hangs, or unexpected behaviour, for investigation.

When fuzzing finds a problem, the response should be to fix the underlying validation or handling bug, and to add a regression test, using the problematic input as a test case, so that the bug does not return in a later version, as discussed for debugging generally. That regression test, with the fuzzing input saved as a test file, turns a one-off fuzzing find into a lasting protection, because future changes will be tested against that input, and a reintroduced bug will be caught immediately, rather than requiring another fuzzing run to rediscover it.

Fuzzing should be bounded, with limits on time, on input size, and on resources, so that it does not itself become a denial of service, by consuming all available processor time or disk space with generated test cases, or by filling logs with crash reports. Running fuzzing for a fixed period, such as a few minutes for a small program, or overnight for a larger one, with a limit on the number of test cases, and with cleanup of generated files afterwards, keeps fuzzing manageable and focused, rather than allowing it to run indefinitely and produce an overwhelming number of findings, many of which may be duplicates of the same underlying bug.

Finally, fuzzing should be combined with other testing, not used as a replacement for validation tests, authorisation tests, or code review, because fuzzing finds robustness bugs, but it does not verify that a program's logic is correct, that its authorisation checks are present, or that its security design is sound, which require targeted tests and review, as discussed earlier.

Check: How does defensive fuzzing differ from attempting to attack a live system?

Section 46

Programs that are used by children, or by other vulnerable users, such as elderly users or users with limited technical experience, have additional security and privacy considerations, because those users may be less able to recognise risks, to configure protections, or to respond to security warnings, and they may be more trusting of prompts and messages that appear to come from a program. A program designed for such users should therefore be secure by default, with minimal configuration required, with clear, simple warnings, and with protections that do not rely on the user making a correct security decision, such as choosing a strong password or recognising a phishing message, because relying on vulnerable users to make such decisions is unfair and unreliable, and it shifts responsibility from the program's designer to its least able users, which is a poor security design.

Data collection for programs used by vulnerable users should be especially minimal, because those users may not understand what data is being collected, or may not be able to consent meaningfully to its collection, even if a consent prompt is shown, if the prompt is complex or if the user feels pressured to accept in order to use the program. Collecting only data that is essential for the program's purpose, explaining that collection in simple, clear language, and providing easy ways to delete data or to stop collection, are practices that respect vulnerable users' privacy, even when legal requirements for consent are technically met by a prompt, because technical compliance alone does not ensure that a user truly understands or agrees to data collection, especially for users who are young, elderly, or otherwise vulnerable.

Interface design for vulnerable users should avoid dark patterns, which are design choices that trick or pressure users into actions they would not otherwise take, such as sharing data, enabling a feature, or making a purchase, through confusing wording, pre-checked boxes, or difficult-to-find opt-outs. Avoiding dark patterns is both an ethical and a security practice, because a user tricked into sharing data or enabling a feature has not genuinely consented, and their data may be exposed or their device may be put at risk, even if the program's code is technically secure. Designing interfaces with clear, honest wording, with opt-in rather than opt-out for sensitive actions, and with easy ways to undo an action, protects vulnerable users from manipulation, whether deliberate or accidental, through confusing design.

Parental or guardian controls, where appropriate for programs used by children, should be designed to be effective without being easily bypassed by the child, such as by using a separate password or by requiring an action that a child cannot easily perform, while still being usable by a guardian who may not be technical, through clear instructions and simple settings.

Finally, programs for vulnerable users should be tested for security and privacy with those users in mind, by considering how a vulnerable user might interact with the program, what mistakes they might make, and what a malicious actor might do to exploit those mistakes, such as by sending a convincing message that appears to come from the program, or by tricking a user into revealing a password.

Check: Why should programs for vulnerable users not rely on those users making correct security decisions?

Section 47

Publishing open data, such as datasets for research, for transparency, or for public use, involves security and privacy considerations, because a dataset that appears anonymous may still allow individuals to be identified, especially when combined with other publicly available data, as discussed for anonymisation generally. Before publishing a dataset, its publisher should assess re-identification risk, by considering what other data is publicly available that could be linked to it, what unique combinations exist in it, and whether a motivated person could use those combinations to identify individuals, even if direct identifiers, such as names, have been removed.

Techniques for reducing re-identification risk in published data include removing direct identifiers, aggregating data to larger groups, suppressing small groups where an individual could be identified, and generalising values, such as reporting age ranges rather than exact ages, or reporting locations at a regional level rather than at a street level. Those techniques, applied together, can significantly reduce risk, while still preserving much of a dataset's usefulness for analysis, provided that the techniques are chosen based on the dataset's specific risks, rather than applied blindly, because over-generalisation can destroy usefulness, while under-generalisation can leave individuals identifiable, even after direct identifiers are removed.

Documentation for published open data should explain what was done to protect privacy, in general terms, without revealing details that would help re-identification, such as exactly which records were suppressed, or exactly how values were generalised, if that detail itself could be used to reverse the protection. That documentation, which might state that direct identifiers were removed, that small groups were suppressed, and that the dataset was assessed for re-identification risk, helps users understand the dataset's limitations and its privacy protections, without providing a roadmap for attacking those protections, which is a balance between transparency and security that is familiar from security documentation generally, where limitations are described honestly, but exploitable details are not disclosed.

Licensing and terms for open data also matter for security and privacy, because a licence that allows any use, without restrictions, may allow a dataset to be used in ways its publisher did not intend, such as for re-identification attempts, or for combining it with other data to profile individuals, even if such uses are not illegal, they may be unethical or harmful. Choosing a licence that restricts harmful uses, where such a licence is available and enforceable, or, where it is not, documenting intended uses and warning against re-identification attempts, in the dataset's documentation, helps set expectations for users, even if it cannot technically prevent misuse, because most users will respect clear guidance, and those who do not will at least have been warned that re-identification is not an intended or acceptable use.

Finally, open data publishing should include a process for responding to re-identification concerns, such as a contact point for reporting a suspected identification, and a plan for withdrawing or updating a dataset if a serious risk is discovered, even though withdrawal cannot recall copies already downloaded, it can prevent further downloads and can warn existing users not to use the dataset for re-identification.

Check: Why cannot published open data be reliably recalled if a privacy risk is later discovered?

Section 48

Security trade-offs involve balancing protection against other goals, such as usability, performance, and development speed, and recognising those trade-offs explicitly helps you make decisions that are reasonable for a program's context, rather than pursuing maximum security at any cost, or sacrificing security entirely for convenience. A classic trade-off is between strong authentication and usability: requiring a long, complex password, plus a second factor, plus frequent re-authentication, provides strong protection, but it may frustrate users, leading them to choose weaker passwords, to write passwords down insecurely, or to avoid using a program altogether, which can reduce overall security in practice, even though the theoretical protection is stronger.

Performance trade-offs are similar: encryption, hashing, and other security measures add computational cost, which can slow a program, especially for large data or for many operations, and that cost must be weighed against the protection gained, and against the program's performance requirements. For most programs, the cost of standard security measures, such as encrypted transport or password hashing, is acceptable, because it is small compared with overall processing, and because the protection it provides is essential, but for programs with extreme performance requirements, such as high-throughput servers, security costs may need to be optimised, such as by using efficient algorithms, by caching securely, or by offloading encryption to specialised hardware, rather than by disabling security altogether, which would be a poor trade, sacrificing essential protection for speed, when optimisation could achieve both.

Development speed trade-offs also occur, where a secure implementation takes longer to write and test than a quick, insecure one, such as using a safe library with proper validation, versus constructing a query or a command by hand, which is faster to code but riskier. Investing in secure practices from the start, even if it slows initial development slightly, is usually faster overall, when total lifecycle cost, including maintenance and incident response, is considered, which is a long-term view that security trade-off decisions should adopt, rather than focusing only on immediate delivery speed.

Trade-off decisions should be documented, with reasons, so that future maintainers understand why a particular balance was chosen, and can revisit it if circumstances change, such as if a program's data becomes more sensitive, or if its performance requirements change, making a previously acceptable trade-off no longer reasonable.

Finally, trade-offs should be revisited periodically, as threats, technology, and user expectations evolve, because a balance that was reasonable when a program was first written may become unreasonable later, such as when a new attack technique makes a previously adequate protection insufficient, or when a new, efficient security method makes a previously costly protection cheap. That periodic revisiting, as part of maintenance and review, ensures that security trade-offs remain appropriate, rather than becoming stale decisions that no longer reflect current risks or capabilities, and it is a practical application of the continuous improvement principle that runs through this chapter: security is not a one-time decision, but an ongoing practice of balancing protection against other goals, with evidence and judgement, over a program's lifetime.

Check: Why can stronger authentication measures sometimes lead to weaker real-world security?

Section 49

A case study in securing a small tool brings together many ideas from this chapter in a concrete, end-to-end example. Imagine a small command-line tool that reads a CSV file containing personal data, such as names and email addresses, summarises it by category, and writes a report, without sending data anywhere else. Its threat model, as discussed at the start of this chapter, identifies its assets as the personal data in its input and output, its inputs as a file supplied by a user, and its actions as reading, summarising, and writing, with risks including disclosure of personal data through output files with broad permissions, through logs that record personal data, or through a report that reveals individuals in small categories, as discussed for analysis privacy.

Design decisions for this tool follow from its threat model. Input validation checks that the input file exists, that it has expected headers, and that its size is within a reasonable limit, before processing, to prevent malformed or huge files from causing problems. Output files are created with restrictive permissions, so that only the tool's user can read them, and the report aggregates data to categories, suppressing small groups, to avoid revealing individuals, as discussed for analysis privacy. Logs record counts and error types, not personal data, and the tool's help text and readme explain that it handles personal data, that output files contain personal data and should be protected, and that input files should not be shared without consideration, because they contain personal data that the tool does not anonymise by default, unless aggregation suppresses it in output.

Implementation of this tool uses safe libraries throughout: a CSV parser for input, rather than manual splitting, parameterised handling for any data storage, though this tool stores only in files, and standard file handling with path validation, to ensure that output paths stay within an intended folder, as discussed for file security. It uses no network access, no external services, and no secrets, which simplifies its security, by reducing its attack surface, as discussed for threat modelling, and that simplicity is itself a security decision, because a tool that does not need network access should not include it, merely because a library makes it easy, since every additional capability is a potential new risk, even if it is not used in the tool's main workflow.

Testing this tool includes validation tests, with malformed, empty, and oversized files, authorisation tests, though for a local tool these are mainly about file permissions, by checking that output files have restrictive permissions, and privacy tests, by checking that reports do not reveal individuals in small categories, using test data with known small groups, to confirm that suppression works.

Finally, maintenance for this tool includes keeping its dependencies updated, reviewing its threat model when its features change, such as if it gains a network feature or a new output format, and updating its documentation to reflect any security-relevant changes, as discussed for maintenance generally. That maintenance, while it may seem disproportionate for a small tool, is proportionate to its handling of personal data, because even a small tool that mishandles personal data can cause significant harm to the individuals whose data it processes, and its author has a responsibility to protect that data, however small the tool may be.

Check: In the case study, why does the tool suppress small groups in its report?

Section 50

This chapter has taken a defensive approach to security, covering the principles, practices, and habits that help Python developers write programs that protect data, respect users, and resist misuse, without teaching attack techniques or bypass methods, which are outside the scope of defensive programming for most developers. You have learned to model threats, validate input, protect secrets, authenticate and authorise users, handle data and files safely, use safe libraries and dependencies, design secure defaults, test defensively, and respond to incidents, each of which contributes to a layered defence, where no single practice is sufficient alone, but together they make programs significantly harder to misuse, whether through mistake or through deliberate attack. That layered, defensive approach is the chapter's central message, and it should guide your future programming, whatever kind of program you write.

The most important habit from this chapter is to think about security during design, not after implementation, because design decisions, such as what data to collect, what inputs to accept, what permissions to require, and what defaults to choose, determine a program's security posture more than any later fix, which can only mitigate a poor design, not transform it into a good one. That design-first habit, applied to every new program by asking a few threat modelling questions before coding, will improve your programs' security more than any single technique, because it ensures that security is considered when it is cheapest and most effective to address, at the start, rather than when it is most expensive and least effective, after release, when changes may break users and require urgent fixes.

As you move on, carry the security checklist from this chapter into your other work, using it during code review, before release, and during maintenance, to check input, secrets, data, authentication, dependencies, and documentation, for every program, not just for those that are obviously security-critical, because every program that handles data, accepts input, or performs actions has security implications, even if they are modest, and checking them routinely prevents small oversights from becoming significant problems. That routine checking, combined with continuous learning about new threats and new protections, will keep your security skills current, and it will make secure programming a habit, rather than a hurdle, in your development work, which is the ultimate goal of this chapter: not to make you a security specialist, but to make you a developer for whom security is a normal, integrated part of writing good software.

A developer who can explain, in plain language, why a program validates a particular input, why a particular default was chosen, and what would happen if a protection failed, has understood security well enough to maintain it, not just to implement it once.

Security, practised this way, becomes part of good programming rather than a separate burden.

Check: What is the chapter's central message about defensive security?

Practical Task

Audit a small program you have written for security using the checklist in this chapter. Check its inputs, secrets, logs, file paths, output permissions, dependencies, and defaults, and record each finding with its severity and fix. Fix at least three findings, test the program again with invalid and boundary inputs, and write a short note explaining one trade-off you made between security and usability or performance.

Chapter Questions

  1. Explain threat modelling, defence in depth, and least privilege, and how they work together in a secure design.
  2. Why must secrets stay out of code and logs, and what should a program do if a secret is exposed?
  3. Describe how input validation, safe libraries, and output encoding together protect a program that handles untrusted data.
  4. A program accepts a file path from a user and writes a report to that path. What security checks should it perform, and why?
  5. Design a secure version of a small tool you use: state its threat model, its safe defaults, and how you would test its security defensively.