Stage 4 of 4, lesson 29 of 31. Improver
Keep settings and secrets out of your code
Read every setting in one function, from environment variables with safe defaults, refuse a wrong value before any request is made, and never print a secret. The secret here is a made up name with nothing in it.
Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.
You need: The lesson on keys and demo accounts. An internet connection. No broker account. No key.
A program has two kinds of value that do not belong in its code. Settings change from one machine or one day to the next: which address to call, how long to pause, which instrument. Secrets must never be seen: a key, a password, a token. Both belong in the environment, which is a set of named text values your terminal hands to every program it starts.
To set one for the current terminal on Windows PowerShell, type $env:PRACTICE_INSTRUMENT = "TIDE" and press Enter. In the older Command Prompt it is set PRACTICE_INSTRUMENT=TIDE. On a Mac or Linux it is export PRACTICE_INSTRUMENT=TIDE. Then run the script from that same terminal. Close the terminal and the value is gone.
The script reads everything in one function, load_settings, and nowhere else. The lines at the bottom, which fetch a price and then a history, name no address, no instrument and no pause of their own. Three habits are in it. Every setting has a default, so the program runs with nothing set. The environment only holds text, so a number is converted and a wrong one is refused with a message that names the variable. And the address is checked, so that a slip cannot send your requests over a connection that is not encrypted.
load_settings takes the environment as an argument. That is what lets the check call it with made up dictionaries and get the same answers on any machine, before the real environment is read.
EXAMPLE_DEMO_SECRET is a made up name, standing in for a key. Leave it unset. The script never sends it and never prints it: describe turns it into set or not set. When you do have a real key, for a demo account, this is the only way your code should ever meet it. Do not put it in the file, and do not type it into any website but your broker's own.
The practice API this lesson calls
Practice data, not any broker's API: fixed files of invented values on this site. Each opens in a browser tab, so you can read what the script will be sent.
The code
# Keep settings and secrets out of your code: read them from environment variables, with safe defaults.
# The practice API is a set of fixed files of invented values. It is not any broker's API.
# It needs no account and no key. EXAMPLE_DEMO_SECRET is a made up name: leave it unset.
import json
import os
import time
import urllib.request
DEFAULT_BASE = "https://openbrokerapi.com/mock/v1"
def load_settings(environment):
"""Read every setting in one place, from a dictionary of names and text values.
Anything missing gets a default. Anything wrong is refused here, before a request is made."""
settings = {
"base": environment.get("PRACTICE_API", DEFAULT_BASE).rstrip("/"),
"instrument": environment.get("PRACTICE_INSTRUMENT", "HBR100").upper(),
"secret": environment.get("EXAMPLE_DEMO_SECRET"), # None when it is not set
}
text = environment.get("PRACTICE_PAUSE_SECONDS", "1")
try:
settings["pause"] = float(text) # the environment only holds text
except ValueError:
raise ValueError("PRACTICE_PAUSE_SECONDS must be a number, not " + repr(text)) from None
if settings["pause"] < 0:
raise ValueError("PRACTICE_PAUSE_SECONDS cannot be less than 0")
if not settings["base"].startswith(("https://", "http://127.0.0.1", "http://localhost")):
raise ValueError("PRACTICE_API must start with https://, unless it is a copy on this machine")
return settings
def describe(settings):
"""The settings as they may be printed. A secret is never shown, only whether there is one."""
shown = dict(settings)
shown["secret"] = "set" if settings["secret"] else "not set"
return shown
# The check, on made up dictionaries, so it gives the same answer on every machine.
assert load_settings({})["pause"] == 1.0
assert load_settings({})["base"] == DEFAULT_BASE
assert load_settings({"PRACTICE_INSTRUMENT": "tide"})["instrument"] == "TIDE"
assert describe(load_settings({"EXAMPLE_DEMO_SECRET": "not-a-real-secret"}))["secret"] == "set"
for wrong in ({"PRACTICE_PAUSE_SECONDS": "soon"}, {"PRACTICE_PAUSE_SECONDS": "-1"}, {"PRACTICE_API": "http://example.com"}):
try:
load_settings(wrong)
except ValueError as problem:
print("Refused:", problem)
else:
assert False, "a wrong setting was accepted: " + str(wrong)
print("Check passed: defaults, a changed setting, a hidden secret and three refusals.")
# Now the real environment. Nothing below names an address, an instrument or a secret.
settings = load_settings(os.environ)
shown = describe(settings)
print("Instrument:", shown["instrument"])
print("Pause between calls:", shown["pause"], "seconds")
print("Secret:", shown["secret"])
def get(path):
request = urllib.request.Request(settings["base"] + path, headers={"Accept": "application/json", "User-Agent": "settings-lesson"})
with urllib.request.urlopen(request, timeout=10) as reply:
data = json.load(reply)
assert data["practice"] is True, "this reply is not practice data"
return data
price = get("/prices/" + settings["instrument"] + ".json")["price"]
print(price["instrumentName"], "bid", price["bid"], "offer", price["offer"])
time.sleep(settings["pause"])
history = get("/history/" + settings["instrument"] + ".json")
print("History:", history["count"], "daily candles, the latest dated", history["candles"][-1]["date"])What you should see
Refused: PRACTICE_PAUSE_SECONDS must be a number, not 'soon' Refused: PRACTICE_PAUSE_SECONDS cannot be less than 0 Refused: PRACTICE_API must start with https://, unless it is a copy on this machine Check passed: defaults, a changed setting, a hidden secret and three refusals. Instrument: HBR100 Pause between calls: 1.0 seconds Secret: not set Harbour 100 (invented index) bid 5057 offer 5058 History: 10 daily candles, the latest dated 2026-01-05
Run with Python 3.12 on Windows against a copy of the practice API files served over HTTP on the same machine, with PRACTICE_API set to the address of the copy. They are the files this site serves. No account and no key.
Check yourself
Answer each question from the code above before you open it. The answers were written from the script on this page and what it printed.
The default pause is written as the text "1", not the number 1. Why?
A value from the environment is always text. Writing the default as text too means one line, float(text), converts both, and the same check for a bad value covers them.
What would the Secret line print if EXAMPLE_DEMO_SECRET were set?
Secret: set. describe replaces the value with the word set or the words not set, so the value itself never reaches print.
Why is http://example.com refused as an address?
load_settings accepts an address that starts with https://, or one that points at a copy on this machine. Anything else raises ValueError, so requests cannot be sent to another machine without encryption by mistake.
Exercise
Add a setting, PRACTICE_TIMEOUT_SECONDS, with a default of 10. Refuse a value that is not more than 0, use the setting in place of the 10 in get, and print it after the Secret line.
What the changed script prints
Refused: PRACTICE_PAUSE_SECONDS must be a number, not 'soon' Refused: PRACTICE_PAUSE_SECONDS cannot be less than 0 Refused: PRACTICE_API must start with https://, unless it is a copy on this machine Check passed: defaults, a changed setting, a hidden secret and three refusals. Instrument: HBR100 Pause between calls: 1.0 seconds Secret: not set Timeout: 10.0 seconds Harbour 100 (invented index) bid 5057 offer 5058 History: 10 daily candles, the latest dated 2026-01-05
Captured from a run of the solution on . Run with Python 3.12 on Windows against a copy of the practice API files served over HTTP on the same machine, with PRACTICE_API set to the address of the copy. They are the files this site serves. No account and no key.
One solution
# Exercise solution: the lesson script with the one change the exercise asks for.
# Keep settings and secrets out of your code: read them from environment variables, with safe defaults.
# The practice API is a set of fixed files of invented values. It is not any broker's API.
# It needs no account and no key. EXAMPLE_DEMO_SECRET is a made up name: leave it unset.
import json
import os
import time
import urllib.request
DEFAULT_BASE = "https://openbrokerapi.com/mock/v1"
def load_settings(environment):
"""Read every setting in one place, from a dictionary of names and text values.
Anything missing gets a default. Anything wrong is refused here, before a request is made."""
settings = {
"base": environment.get("PRACTICE_API", DEFAULT_BASE).rstrip("/"),
"instrument": environment.get("PRACTICE_INSTRUMENT", "HBR100").upper(),
"secret": environment.get("EXAMPLE_DEMO_SECRET"), # None when it is not set
}
text = environment.get("PRACTICE_PAUSE_SECONDS", "1")
try:
settings["pause"] = float(text) # the environment only holds text
except ValueError:
raise ValueError("PRACTICE_PAUSE_SECONDS must be a number, not " + repr(text)) from None
if settings["pause"] < 0:
raise ValueError("PRACTICE_PAUSE_SECONDS cannot be less than 0")
settings["timeout"] = float(environment.get("PRACTICE_TIMEOUT_SECONDS", "10"))
if settings["timeout"] <= 0:
raise ValueError("PRACTICE_TIMEOUT_SECONDS must be more than 0")
if not settings["base"].startswith(("https://", "http://127.0.0.1", "http://localhost")):
raise ValueError("PRACTICE_API must start with https://, unless it is a copy on this machine")
return settings
def describe(settings):
"""The settings as they may be printed. A secret is never shown, only whether there is one."""
shown = dict(settings)
shown["secret"] = "set" if settings["secret"] else "not set"
return shown
# The check, on made up dictionaries, so it gives the same answer on every machine.
assert load_settings({})["pause"] == 1.0
assert load_settings({})["base"] == DEFAULT_BASE
assert load_settings({"PRACTICE_INSTRUMENT": "tide"})["instrument"] == "TIDE"
assert describe(load_settings({"EXAMPLE_DEMO_SECRET": "not-a-real-secret"}))["secret"] == "set"
for wrong in ({"PRACTICE_PAUSE_SECONDS": "soon"}, {"PRACTICE_PAUSE_SECONDS": "-1"}, {"PRACTICE_API": "http://example.com"}):
try:
load_settings(wrong)
except ValueError as problem:
print("Refused:", problem)
else:
assert False, "a wrong setting was accepted: " + str(wrong)
print("Check passed: defaults, a changed setting, a hidden secret and three refusals.")
# Now the real environment. Nothing below names an address, an instrument or a secret.
settings = load_settings(os.environ)
shown = describe(settings)
print("Instrument:", shown["instrument"])
print("Pause between calls:", shown["pause"], "seconds")
print("Secret:", shown["secret"])
print("Timeout:", shown["timeout"], "seconds")
def get(path):
request = urllib.request.Request(settings["base"] + path, headers={"Accept": "application/json", "User-Agent": "settings-lesson"})
with urllib.request.urlopen(request, timeout=settings["timeout"]) as reply:
data = json.load(reply)
assert data["practice"] is True, "this reply is not practice data"
return data
price = get("/prices/" + settings["instrument"] + ".json")["price"]
print(price["instrumentName"], "bid", price["bid"], "offer", price["offer"])
time.sleep(settings["pause"])
history = get("/history/" + settings["instrument"] + ".json")
print("History:", history["count"], "daily candles, the latest dated", history["candles"][-1]["date"])Your progress
Your marks are kept in this browser only, in its local storage. They are not sent anywhere, there is no account, and they will not follow you to another browser or device. To remove them, use Clear my progress on the learning track page, or clear this site's data in your browser's settings. Marking needs JavaScript. With scripts off the track reads the same, without the ticks.
This browser refused to store anything for this page, so marks cannot be kept here.
Where this sits in the path
Stage 4 of 4: Building things that survive. Failure, limits, repeated orders, logs, settings, tests and backtests that do not fool you: what separates a script from a tool.
- Retry and backoffDone
- Stay under a rate limitDone
- Handle an error replyDone
- Idempotent ordersDone
- Log what happenedDone
- Log your API callsDone
- Backtest an ideaDone
- The look ahead mistakeDone
- Settings from environmentDoneYou are here
- Test with saved repliesDone
- A command line toolDone
- The capstone projectDone