The Problem: Priya's Leaked Key
Priya is building a weather-checker for her Class 8 project. She signs up on a weather-data website, and it gives her a personal API key — a long secret code like a91f0c3e77b2 — that she must send along with every request so the server knows the request is coming from her account and not from a stranger. She writes a short Python program, tests it, and it works beautifully. Proud of it, she pushes the file to GitHub so her classmates can see her code.
The next morning, her program stops working. When she checks her account on the weather website, she finds her daily quota of 1,000 free requests was used up within minutes of her pushing the code — by someone she has never met. What happened?
Her mistake is sitting right there in the file she uploaded:
api_key = "a91f0c3e77b2"
url = "https://api.weatherservice.com/data?city=Bengaluru&key=" + api_key
The secret key is typed directly into the source code, as plain text, inside a file she just made public. The moment that file went onto GitHub, anyone in the world could open it and read the key. In practice, this happens so often that GitHub runs automated scanners that look for exactly this pattern — strings that look like API keys sitting inside public code — because leaked keys get scraped and abused within minutes of being pushed.
Priya's actual mistake was not "trusting the internet." Her mistake was mixing two things that should never live in the same place: the logic of her program (fetch weather data, print the temperature) and a secret value that only her own computer should know. This chapter is about the tool professional programmers use to keep those two things separate — environment variables.
An Analogy Before the Definition: The Script and the Stage
Think about a play performed in a theatre. The script tells the actors exactly what to say and when to enter and exit — "Enter from stage left. Say the line. Exit stage right." The script is written once and photocopied for every performance, in every city, on every night.
But the script never says "turn on spotlight number 47" or "set the sound system volume to 60%." Those details depend on which theatre the play is being performed in tonight — a school auditorium in Pune has a different lighting rig than a mid-size hall in Chennai. So instead of hard-coding lighting instructions into the script, the director simply says to the actor, "when you reach this line, ask the stage manager for the cue," and the stage manager — who knows the specific theatre they're in tonight — supplies the right answer for that venue.
The script is your program's source code: the same file, copied everywhere, never changing. The stage manager's answers are the environment — information that belongs to wherever the program happens to be running right now, supplied at the moment it's needed rather than baked into the script itself. An environment variable is exactly this: a named piece of information that lives outside your program's code, in the environment the program is running in, which the program can ask for while it runs.
What an Environment Variable Actually Is
Contrast this with a normal variable you already know from Python, like city = "Bengaluru". That line lives inside your .py file. Every single time anyone runs that file, on any computer, city is "Bengaluru" — because the value is written directly into the text of the program. To change it, you must edit the file itself.
An environment variable is different in three important ways:
- It is not written inside your source-code file at all. It is stored by the operating system (or the command-line shell you're typing into), completely separate from your
.pyfile. - Your program does not "have" it automatically the way it has a variable you assigned with
=. It must actively ask the operating system for the value while it is running, using a function call. - The very same unedited source file can get a different answer on a different computer, or even a different terminal window, because the value comes from outside the file, not from inside it.
This is the whole point: the code that says "go fetch the weather using this API key" can be shared, copied, and made public safely, as long as the actual key itself never appears as text inside that code.
Where Environment Variables Actually Live
Every environment variable you'll meet on a normal computer already exists before you write a single line of your own code — the operating system sets up dozens of them the moment you open a terminal. One of the most important is called PATH. Try this in any terminal:
$ echo $PATH
/usr/local/bin:/usr/bin:/bin
PATH is a single environment variable whose value is a list of folder locations, separated by colons on Linux/macOS (semicolons on Windows). It answers a question you've never had to think about: when you type python3 and press Enter, how does your computer know where the python3 program actually is on your hard disk?
Here is exactly what happens, step by step, when you type python3:
- The shell reads the value of the
PATHenvironment variable:/usr/local/bin:/usr/bin:/bin. - It splits this string on the colon character into a list:
["/usr/local/bin", "/usr/bin", "/bin"]. - It checks the first folder: does a file named
python3exist inside/usr/local/bin? If not, move to the next folder. - It checks
/usr/bin. Supposepython3exists there — the search stops, and that program is launched.
Nobody typed the full path /usr/bin/python3. The environment variable did the lookup work. This is the same mechanism your program will use later — asking the environment for a named value instead of having it hard-coded — just applied by the shell instead of by your Python script.
Setting and Reading a Variable: A Worked Example
Let's set our own environment variable and read it from Python, tracing exactly what happens. In a terminal (bash/zsh, common on macOS, Linux, and inside tools like Replit):
$ export WEATHER_API_KEY="a91f0c3e77b2"
export tells the shell: create an environment variable named WEATHER_API_KEY with this value, and make it available to any program launched from this shell from now on. Now, in a Python file run from that same terminal:
import os
api_key = os.environ.get("WEATHER_API_KEY")
if api_key is None:
print("Error: set WEATHER_API_KEY before running this program.")
else:
print("Using key that starts with:", api_key[:4])
Let's trace this line by line. os.environ is a dictionary-like object that Python automatically fills with every environment variable visible to this process the moment it starts. os.environ.get("WEATHER_API_KEY") looks up that key in the dictionary and returns "a91f0c3e77b2" — the exact string we exported a moment ago. Since it is not None, the program takes the else branch. api_key[:4] slices the first four characters of the string: "a91f". The program prints:
Using key that starts with: a91f
Notice we used .get(...) rather than os.environ["WEATHER_API_KEY"]. If the variable had never been exported, os.environ["WEATHER_API_KEY"] would crash the program with a KeyError. .get(...) instead returns None quietly, which is why we can check if api_key is None and print a friendly error instead of a crash.
Fixing Priya's Leak
Now Priya can rewrite her program using this pattern instead of typing the key into the file:
import os
api_key = os.environ.get("WEATHER_API_KEY")
city = "Bengaluru"
url = "https://api.weatherservice.com/data?city=" + city + "&key=" + api_key
This .py file contains zero secrets. It can be pushed to a public GitHub repository, shown to classmates, and printed in her project report, and nobody can extract her key from it — because the key was never written into it in the first place. The key only exists as an environment variable on her own laptop, set once with export, invisible to anyone who only ever sees her source code.
Of course, this means the program will refuse to run on a fresh computer until someone sets WEATHER_API_KEY there too. That's expected, and it's the correct trade-off: a program that fails safely with a clear error message is far better than one that leaks a secret to the whole internet. (In real professional projects, developers often keep a local file, commonly named .env, holding these values for their own machine, and deliberately exclude that one file from what gets uploaded to GitHub — the code goes public, that one file never does.)
A Second Job for Environment Variables: Same Code, Different Behaviour
Secrets are only one use. The far more common everyday use of environment variables is letting the exact same, unedited program behave differently depending on where it's running — without a programmer touching a single line of code to switch it.
Suppose Priya wants extra debugging messages while she's writing and testing her program, but she doesn't want those messages cluttering the output once she shares the finished version with her teacher:
import os
debug_mode = os.environ.get("DEBUG", "False") == "True"
if debug_mode:
print("[debug] connecting to weather server...")
print("Temperature in Bengaluru: 27C")
Look closely at os.environ.get("DEBUG", "False"). The second argument is a default value: if DEBUG was never set as an environment variable at all, .get returns "False" instead of None, so the program never crashes and quietly assumes debugging is off. If Priya runs the program normally, with nothing set, debug_mode becomes "False" == "True", which evaluates to False, and only the temperature line prints. If, while testing, she runs export DEBUG="True" first, debug_mode becomes "True" == "True", which is True, and the extra debug line appears too. She never edited the program to switch modes — she only changed her environment.
Misconception #1: "An Environment Variable Is Basically the Same as a Python Variable"
This is one of the most common mix-ups students make, and it's worth correcting precisely. A Python variable created with =, like port = 8080, can hold any type — an integer, a string, a list, a boolean. An environment variable can only ever hold text. Even if the value looks like a number, Python receives it from os.environ as a string, never as an integer. Watch what goes wrong if you forget this:
$ export PORT=8080
import os
port = os.environ.get("PORT")
print(type(port))
print(port + 1)
Tracing this: os.environ.get("PORT") returns the string "8080", not the integer 8080 — the environment stores plain text, full stop, no exceptions. So print(type(port)) prints <class 'str'>. The next line, port + 1, tries to add the integer 1 to a string, which Python refuses to do, and the program crashes with:
TypeError: can only concatenate str (not "int") to str
The fix is to explicitly convert the string to a number before using it as one:
port = int(os.environ.get("PORT", "8080"))
print(port + 1)
Now os.environ.get(...) still returns the string "8080", but int(...) converts it to the integer 8080 before it's stored in port. port + 1 now correctly evaluates to 8081. This single detail — everything from the environment arrives as a string and must be deliberately converted — trips up even experienced programmers, so it's worth remembering as a rule, not a one-off gotcha.
Misconception #2: "If I Set It in One Terminal, It's Set Everywhere"
The second common misconception is about scope. Students often assume that running export WEATHER_API_KEY="a91f0c3e77b2" makes that variable permanently exist on the computer, visible to every program from now on. It does not. An environment variable set with export lives only inside that one running shell session. If you open a second terminal tab, or a second terminal window, it starts with a fresh copy of the system's default environment and will not have your new variable at all — echo $WEATHER_API_KEY there prints nothing.
The precise rule is: when a program starts, the operating system hands it a copy of its parent process's environment variables, frozen at that exact moment. A shell is a process too. If you export a variable and then launch a Python program from that same shell, the Python program inherits the copy, including your new variable. But sibling terminal windows, programs that were already running before you typed export, and any terminal you open afterward are unaffected, because they never received that copy.
This is precisely why professional setups avoid retyping export every time: they place these lines in a shell start-up file (such as ~/.zshrc or ~/.bashrc) that runs automatically every time a new terminal opens, or use a project-local .env file that a small library loads automatically when the program starts. Both approaches exist purely to solve this one scoping problem — making sure the right process gets the right copy, every time, without you doing it by hand.
How a Program's Environment Is Built
The diagram below shows the actual mechanism: the shell holds its own environment as a set of name-value pairs. When you launch a program from that shell, the operating system copies those pairs into the new process's own private environment at the exact instant it starts. A second, unrelated terminal has its own separate environment, with no connection to the first.
The same unedited weather.py file produces two different results in the two boxes on the right — not because the code changed, but because the environment each process was born into was different. This is the entire mental model you need: environment variables are not part of your program, they are part of the process's birth certificate, copied once from its parent at the moment it starts.
Real Practice: Why This Matters Beyond School Projects
This exact pattern is standard practice at real companies, including Indian ones. Payment gateway providers used widely by Indian startups, such as Razorpay, issue developers two separate pairs of API keys — one marked "test mode" for trying things out with fake transactions, and one marked "live mode" for real money. Developers store whichever pair is appropriate as environment variables, and the exact same application code switches from harmless testing to real payments simply by which environment it's deployed into — never by editing the program. The same idea extends to database passwords, cloud storage credentials, and third-party service keys across almost every production application you'll encounter, in India or anywhere else: the code is one fixed, shareable thing; the secrets and settings that make it behave for a specific place and purpose are supplied separately, at run time, through the environment.
This is also precisely why, in the CBSE Computer Science and Informatics Practices curriculum, you'll keep meeting the idea that "configuration" and "logic" should be kept apart as programs grow larger — environment variables are the simplest, most universal tool for doing that separation in practice, and the one you are most likely to actually type yourself the first time you deploy something, share code publicly, or work with any API key.
Check Your Understanding
- A classmate writes
city = "Chennai"at the top of their Python file and calls it "using an environment variable." Explain precisely why this is incorrect, using the definition from this chapter. - You run
export MAX_SCORE=100in your terminal, then run this program from the same terminal:
What does this print, and why is it probably not what the programmer intended? What one change fixes it?import os score = os.environ.get("MAX_SCORE") print(score * 2) - You set
export DEBUG="True"in Terminal A, then open Terminal B and immediately run a Python program there that checksos.environ.get("DEBUG", "False"). What value does it read, and why? - Why is
os.environ.get("API_KEY")generally safer to use thanos.environ["API_KEY"]in a program you plan to share with others who might run it without setting that variable first? - A friend argues, "environment variables keep my secrets encrypted and safe." Identify exactly what is wrong with this claim, based on what this chapter said environment variables actually do and don't provide.
Answer key: (1) city = "Chennai" is a normal Python variable: it is written directly inside the source file's text and will be exactly "Chennai" every time, on every machine, until someone edits the file. A real environment variable is set outside the file, in the shell or OS, and read at run time via os.environ; it can differ per machine or per run without any code changes. (2) It prints 100100, not 200 — because os.environ.get always returns a string, so score is "100", and "100" * 2 repeats the string rather than multiplying a number. The fix is score = int(os.environ.get("MAX_SCORE")) before using it arithmetically. (3) It reads "False", the default value — Terminal B is a separate shell session with its own independent environment, and the export typed in Terminal A was never copied into it. (4) .get(...) returns None quietly if the variable is missing, letting the program print a helpful message; the square-bracket form os.environ["API_KEY"] raises a KeyError and crashes immediately if the variable was never set, which is a much worse experience for anyone trying to run the shared code for the first time. (5) Environment variables are ordinary, unencrypted text, readable by anyone with access to that computer or that process's information — they only solve the problem of keeping secrets out of shared source code; they are not a form of encryption or general security by themselves.
Summary
An environment variable is a named piece of text-only information that lives outside your program's source code, inside the operating system or shell it runs in, and is read at run time — typically in Python via os.environ.get("NAME"), ideally with a default value as a second argument so a missing variable doesn't crash the program. This separation exists for two main reasons: it keeps secrets such as API keys out of source code that might be shared or made public, and it lets the exact same, unedited program behave differently — more debugging output, a test server instead of a live one, a different port number — depending purely on where and how it's launched. Every value that comes from os.environ arrives as a string and must be explicitly converted with functions like int(...) before it can be used in arithmetic. And because each running process only inherits a frozen copy of its parent's environment at the exact moment it starts, a variable exported in one terminal session does not automatically appear in any other terminal, program, or future session — only in processes launched from that same shell, after that export was typed.