What you'll learn
The first three Applied Python lessons established a local environment, a configurable command-line tool and a validated file pipeline. This lesson adds a new boundary: remote services can be slow, reject permissions, send only part of a dataset or return an unexpected response. You'll learn to make those conditions visible instead of publishing an inaccurate report.
- Explain HTTP request methods, resource URLs, headers, status codes and JSON response contracts.
- Distinguish 200 from 204 and authentication failures from permission failures.
- Construct properly encoded URL query strings and enforce an approved HTTPS origin for real remote clients.
- Describe API keys, OAuth access tokens, least-privilege delegated versus application authorization, and secret redaction.
- Separate transport timeouts, HTTP errors, JSON decode errors and malformed response schemas.
- Collect bounded, validated paginated records while rejecting repeated or unapproved next links.
- Interpret 429 throttling and Retry-After guidance using safe, bounded retry policies for read-only requests.
- Connect a fake response provider or optional localhost HTTP mock to deterministic offline inventory reporting.
127.0.0.1 on your computer. It never accesses a cloud tenant and needs no API key.Prerequisites: Complete Filesystem & Structured Data Pipelines and Command-Line Tools & Configuration, or be comfortable with functions, dictionaries, JSON, explicit argument lists and error handling. This lesson is about writing trustworthy read-only clients, not acquiring permissions for a real production API.
Trace a request from method to response
Your CLI can now read and validate local files. Remote systems expose APIs so a program can request information instead of opening another application's data files. An HTTP request specifies a method, URL, headers and sometimes a body. A response supplies a status, headers and optional response body. REST APIs commonly represent resources using URLs and JSON, but each provider defines its own contract. A successful connection is not evidence that you requested the right resource or received the expected data.
GET is the first safe operation
Start with a read-only GET for fictional devices. Other common methods include POST to create, PATCH to modify, PUT to replace and DELETE to remove; method semantics depend on the API contract and permissions.
Resource paths describe targets
A path such as /v1/devices names a collection. A query such as ?site=HQ filters it; the server determines whether that filter is supported and how the response is paginated.
Headers carry protocol metadata
Accept: application/json expresses the content your client expects; Content-Type describes the format of any request body. Authorization is security-sensitive and must not be printed or placed in a URL.
from urllib.parse import urlsplit
url='https://api.example.invalid/v1/devices?site=HQ'
parts=urlsplit(url)
print(parts.scheme, parts.path, parts.query)If the server returns HTTP 200, can you conclude the response contains a usable inventory object? Reveal the explanation after predicting.
No. Status 200 is an HTTP success response, not schema validation. The body might be an array, a different object, an HTML error page or another unexpected shape. Check its content type, decode the body and validate required fields.
Distinguish empty responses, errors and permission failures
HTTP response status changes what your client should do next. A 200 OK response may contain JSON, whereas 204 No Content indicates a successful response without a body, so calling json.loads('') is incorrect. Authentication failures (401) differ from insufficient permission (403). A 404 may indicate a missing resource or an endpoint you cannot see; 429 signals throttling and a 5xx response means the server couldn't complete the request. Don't classify every non-200 response as a network outage, and don't classify every 200 response as a valid application result.
401 versus 403
401 commonly means credentials are missing, expired or invalid and may include an authentication challenge. 403 means the server understood but refuses the request; signing in again is not a reliable fix for missing scope or permission.
429 and 503 aren't the same
429 Too Many Requests indicates rate limiting, sometimes with Retry-After. 503 Service Unavailable indicates a temporary service problem; retry policy depends on request semantics, server guidance and an explicit bound.
204 has no JSON body to parse
For a legitimate 204, handle a no-content success as its own branch. Don't interpret an absent body as a list of zero devices or as a malformed JSON export.
def status_kind(code):
if code == 204: return 'no-content'
if code == 401: return 'authentication'
if code == 403: return 'permission'
if code == 429: return 'throttled'
return 'success' if 200 <= code < 300 else 'error'
print(status_kind(401))Why shouldn't a caller automatically reauthenticate after receiving 403? Reveal the explanation after predicting.
A 403 response usually means the operation is not permitted even if the client has identified itself. Repeating login attempts won't grant missing API scopes or roles. Review the documented authorization requirements with an approved administrator.
What would your Python client do next?
Change the HTTP scenario, requested site and page budget. Inspect how the same read-only request behaves when authentication, permissions, JSON shape, throttling or pagination fail. No API call is made, and there is nowhere to enter real credentials.
Choose the method, resource path and response scenario
{
"method": "GET",
"url": "https://api.example.invalid/v1/devices?site=HQ",
"headers": {
"Accept": "application/json",
"Authorization": "Bearer <redacted fictional placeholder>"
},
"timeoutSeconds": 5
}Every simulated page and device is valid. A local reporter could now prepare a stable output.
This is an explanatory simulation, not a live HTTP client. The Practice clinic executes Python functions against offline injected response fixtures. The optional local project runs an actual HTTP request only against a mock server bound to 127.0.0.1.
Encode query parameters and validate request destinations
Build URLs from a trusted base and encode query parameter values using urllib.parse.urlencode rather than concatenating untrusted strings. Spaces, ampersands and other characters have special roles in URL syntax. A site named 'HQ & East' should remain one query value, not become another parameter. Keep an explicit HTTPS origin allowlist for operational clients and validate hostnames before following URLs or redirects, especially pagination URLs returned by a server. Query strings often appear in proxies, histories and logs; never put access tokens or passwords there.
Escape data, not syntax
urlencode({'site':'HQ & East'}) generates a single encoded value. A literal '&' inserted into a raw URL would change the query structure and could silently alter server-side filters.
Request with a timeout
urllib.request.Request(url, headers={'Accept':'application/json'}, method='GET') describes the outgoing request. urllib.request.urlopen(request, timeout=5) sends it, but only in a real local Python process with network access.
Avoid server-directed arbitrary hosts
An API-provided nextLink or redirect must not make your tool contact an unapproved host or access a local metadata endpoint. Compare parsed scheme and hostname against your approved service origin.
from urllib.parse import urlencode
from urllib.request import Request
url='https://api.example.invalid/v1/devices?'+urlencode({'site':'HQ & East'})
request=Request(url,headers={'Accept':'application/json'},method='GET')
print(request.full_url.endswith('site=HQ+%26+East'))Why is adding an access token to the URL query string a poor substitute for Authorization headers? Reveal the explanation after predicting.
URL query strings are routinely recorded by browser history, proxies, server access logs and monitoring systems. Sensitive credentials should use an approved authentication mechanism, usually an Authorization header over HTTPS, with careful log redaction.
Read every URL property your validator will inspect
Python's urlsplit exposes attributes the safe-continuation exercise needs. The main lesson shows scheme/path/query; the exercise additionally requires hostname, port and credential detection.
from urllib.parse import urlsplit
parts = urlsplit('https://api.example.invalid:443/v1/devices?page=2')
print(parts.scheme)
print(parts.hostname)
print(parts.port)
print(parts.username is None and parts.password is None)
print(parts.path).hostname normalizes the host, .port gives the integer port (or None when absent), and .username/.password reveal embedded credentials, which must be rejected. Accessing .port on some malformed values can raise ValueError; fail closed instead of following an untrusted URL.
Predict and explain: Should `https://user:pass@api.example.invalid/v1/devices` be followed simply because .hostname matches the allowed API?
No. Reject URLs that embed credentials, even when the hostname is correct.
Separate API authentication, authorization and consent
Authentication establishes an identity for a request; authorization determines what that identity can access. An API key identifies a caller only within its provider's defined scheme and may not represent a human user. OAuth 2.0 access tokens typically carry delegated or application permissions and are presented as Bearer tokens, but bearer possession alone grants access only within the token's audience, lifetime and scope. Microsoft Graph scenarios distinguish delegated user context from application-only permissions; an API call should request the minimum approved permissions for its work. Credentials are not necessary for our fictional offline simulator.
401: identify the credential problem
A missing, expired, invalid or wrong-audience token may lead to 401. Do not immediately generate an endless retry loop or paste the token into a ticket or chat transcript.
403: inspect authorized scope
A valid identity may still lack the role or scope required by the endpoint. Treat this as an authorization boundary: review permission grants, consent and the endpoint contract rather than escalating privileges casually.
Least privilege applies to scripts
A read-only reporting client should not request broad write permissions. Application permissions can operate without an interactive user, so their scope and secret storage deserve particular attention.
def classify_auth(status):
return {401:'Check authentication',403:'Review permissions'}.get(status,'Other status')
print(classify_auth(403))An app has a valid token but the API returns 403. Is re-running the same credential flow guaranteed to fix it? Reveal the explanation after predicting.
No. A valid token can lack the appropriate delegated/application permission, role or resource access. Verify the endpoint's authorization requirements and approved grant before changing permissions. Reauthenticating without correcting scope often changes nothing.
Protect tokens, credentials and diagnostic output
A bearer token is sensitive because anyone who obtains a usable one may be able to act with its permissions. Never embed real tokens in source control, lesson examples, browser code or query strings. Local environment variables may be practical for a short demonstration, but they are not a secret vault and may leak through process inspection or CI logs; production tools should use organization-approved credential management, rotating short-lived tokens and approved libraries or SDKs. Build an Authorization header only from a provided token value, avoid printing it, and redact known sensitive fields from errors.
Only fictional tokens in exercises
The browser uses a symbolic placeholder such as <redacted> and never asks for a personal token. There are no real authentication endpoints or credential exchanges in this learning page.
Log the outcome, not the credential
Record an HTTP status and a nonsensitive request identifier, not a bearer token, password, Authorization header or full credential-bearing URL.
Don't overinterpret environment variables
An environment variable can keep a token out of committed source, but processes, crash dumps or build logs may still expose it. Organizational secret stores and token rotation are better operational defaults.
def safe_headers(headers):
return {k:('<redacted>' if k.lower() in {'authorization','x-api-key'} else v)
for k,v in headers.items()}
print(safe_headers({'Authorization':'Bearer DEMO','Accept':'application/json'}))Is printing the Authorization header acceptable if you first replace only the word Bearer? Reveal the explanation after predicting.
No. The sensitive part is the token value after Bearer. Redact the entire sensitive header value, and also avoid accidentally logging query strings or exception objects that could contain credentials.
Handle timeouts and transport errors without hiding them
Even a correct API endpoint can be unreachable, slow or interrupted. `urllib.request.urlopen` accepts a timeout in seconds for blocking operations; failing to provide bounded network timeouts can stall scheduled jobs. Distinguish socket.timeout/TimeoutError and URLError transport failures from HTTPError responses, which carry a status code and often a body. A 200 status paired with invalid JSON is a response validation error, not a network timeout. Keep error messages clear enough to diagnose the stage without exposing secrets or response payloads unnecessarily.
HTTPError is an HTTP response
urllib raises HTTPError on many 4xx/5xx responses. It contains .code and .headers; handle these as service responses instead of telling the operator the internet is down.
A timeout is not zero results
A connection that never completes does not prove there are zero devices. Return a distinct failure state and do not overwrite a previously valid report.
Make transport mockable
A function that accepts a `fetch(url)` callback can be tested offline against response dictionaries and simulated failures. That separation makes retry, validation and status logic independently testable.
def describe_result(result):
if result['kind']=='timeout': return 'Transport timed out'
if result['kind']=='http': return f"HTTP {result['status']}"
return 'Invalid response envelope'
print(describe_result({'kind':'timeout'}))Why must a 503 response and a timeout be represented differently in a reporting workflow? Reveal the explanation after predicting.
A 503 means the server returned an HTTP response indicating service unavailability, whereas a timeout means the client couldn't complete a response in time. Their diagnostic evidence and retry handling differ; neither should become a valid empty inventory.
Follow pagination safely and cap the work
Many directory and IT inventory APIs return only a subset of records per response. Microsoft Graph uses `value` arrays and may provide `@odata.nextLink` when another page exists. A client that reads one 200 response and stops may quietly omit most devices. Validate each page's shape, collect records in a deterministic order, and enforce a maximum number of pages and records. Maintain a set of visited URLs to catch repeated/cyclic links. Validate every nextLink against the approved HTTPS service origin before fetching it; never blindly follow a server-supplied URL to an arbitrary host.
The next link is a full continuation URL
Microsoft Graph often supplies an opaque @odata.nextLink that includes a server-generated paging token. Don't invent or modify that token to fetch later pages.
Stop only on a documented termination
An absent/null nextLink is normal completion. A repeated link, too many pages, invalid response shape or failed page is an error; do not publish a partial inventory as if it is complete.
Limit and verify the totals
Choose a bound such as 10 pages for the training client and check every page's value array. Sorting before reporting creates stable output even when the provider changes page order.
def collect(page1,page2):
return sorted(page1['value']+page2['value'])
print(collect({'value':['SW-02']},{'value':['AP-01']}))Why is it dangerous to trust a @odata.nextLink URL without inspecting its scheme and hostname? Reveal the explanation after predicting.
A returned URL can direct a client away from the approved service to a different or internal host. When requests contain bearer tokens, following an untrusted link could expose credentials or trigger unintended network access. Validate each destination and keep requests on an allowlisted origin.
Validate the next URL before asking a client to fetch it
The API capstone reuses the URL-safety rule on every page. Put the predicate in a helper, check for loops and restrict both the origin and allowed resource path.
from urllib.parse import urlsplit
def allowed_page(url):
try:
p = urlsplit(url)
return (p.scheme == 'https' and
p.hostname == 'api.example.invalid' and
p.port in (None, 443) and
p.username is None and p.password is None and
p.path == '/v1/devices')
except ValueError:
return False
print(allowed_page('https://api.example.invalid/v1/devices?page=2'))
print(allowed_page('https://evil.example/steal'))This helper inspects a destination; it does not fetch the URL. The independent pagination challenge still needs you to enforce page limits, visited-link tracking and all-or-nothing reporting.
Predict and explain: Can validating only the first request URL protect a client that follows an untrusted nextLink from a later page?
No. Every continuation must pass the same destination checks and a cycle/page budget check before it is requested.
Respect 429, Retry-After, backoff and idempotency
APIs protect their capacity by throttling clients. Microsoft Graph commonly responds with HTTP 429 and a Retry-After header containing a suggested wait interval. For a read-only GET, a limited retry with the server's guidance (and a fallback backoff policy when guidance is absent) can be reasonable. Never immediately hammer the endpoint or sleep unboundedly. Parse Retry-After carefully: HTTP permits both delta-seconds and HTTP-date, so the numeric-only helper in this lesson is an explicit simplified training subset. Changing operations such as POST must not be automatically retried without understanding idempotency keys, duplicate creation and the API's contract.
Retry-After gives a minimum wait
For a valid numeric Retry-After: 10, wait at least that many seconds before another request when permitted by your job deadline. Do not treat 10 as the number of attempts.
Cap attempts and time budget
A small bounded attempt count, fixed timeout per call and overall deadline prevent a scheduled job from hanging indefinitely. Record a clean failure after the budget is exhausted.
Never blindly retry writes
A POST might have succeeded even if the client lost its response. Replaying it can create duplicates. Only retry mutations with an endpoint-specific safe idempotency strategy and explicit operational approval.
def retry_seconds(headers):
value = headers.get('Retry-After','')
return min(int(value),30) if value.isdecimal() else None
print(retry_seconds({'Retry-After':'10'}))Would a report tool be correct to retry every 429 response immediately until it receives HTTP 200? Reveal the explanation after predicting.
No. Immediate retries can worsen throttling and consume a service's quota. Honor valid Retry-After guidance, enforce attempt and time limits, and stop when the budget is exhausted. For state-changing requests, retry policies must account for possible duplicate effects.
Connect API responses to your existing JSON pipeline
A practical API-based inventory reporter should use your earlier CLI for approved configuration, request data from a known service with the minimum permissions, interpret HTTP errors, page through complete results, validate JSON records, then build the stable report learned in Lesson 03. Design the core as an offline-testable function that takes an injected response provider, and keep actual network access in a separate adapter. The local example uses a tiny server bound to your own loopback address with fictional responses—no Microsoft tenant, API token, customer data or third-party package needed. After verifying that behavior, learners can adapt it to an authorized sandbox API in later training.
Test failures before any production integration
Simulate 200 valid JSON, 200 wrong shape, 204, 401, 403, 404, 429, 503, malformed paging links and a timeout. A reporting function should explicitly say which evidence is incomplete.
Read-only means read-only
Use GET and fictional fixtures in this lesson. No POST, PATCH or DELETE calls and no real access tokens are required. If a report cannot be completed, avoid replacing the last known-good result.
Keep operational decisions visible
Record how many pages were fetched, which nonsecret request ID was returned, and whether the report was complete. The next lesson expands this into logging, repeatable tests and resilience.
def build_inventory_report(items):
hosts=sorted(item['host'] for item in items)
return {'total':len(hosts),'hosts':hosts}
print(build_inventory_report([{'host':'SW-02'},{'host':'AP-01'}]))What should the reporter do after page 1 succeeds but page 2 fails with a timeout? Reveal the explanation after predicting.
Return an incomplete/error result and preserve any prior known-good report. Do not quietly publish the first page as the whole inventory. Record the timeout stage and continue only according to a bounded, approved retry policy.
Run a real HTTP client against your own mock API
You can practice actual HTTP request and response behavior without a Microsoft tenant, external test account or real credentials. Save the Python script below as local_api_demo.py inside the virtual environment project from Lesson 01. It starts a temporary server on your computer's 127.0.0.1 interface with a random available port, makes GET requests using urllib.request, validates paged JSON, prints a report and shuts the server down. All devices are fictional and no files are modified.
What localhost means
127.0.0.1 is your own computer's loopback interface. The demo uses plain HTTP only on localhost with no credentials. Use HTTPS and an organization-approved authentication flow for actual remote services. If your device security policy blocks local listeners, use the browser exercises instead rather than changing that policy.
Why simulate failures?
Instead of depending on a public API being online, switch the demo to 401, 403, 429, 503 or malformed JSON. Test your program's error path under controlled conditions, then compare what it prints with the explanations above.
"""Offline REST API practice. Binds ONLY to 127.0.0.1; uses fictional inventory."""
import argparse
import json
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.error import HTTPError, URLError
from urllib.parse import parse_qs, urlsplit
from urllib.request import Request, urlopen
DEVICES = [
{"host": "SW-01", "status": "online"},
{"host": "AP-02", "status": "offline"},
{"host": "FW-01", "status": "online"},
]
SCENARIOS = ("normal", "auth", "forbidden", "throttle", "service", "bad-json")
class DemoAPI(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass # Don't print request headers or credentials.
def do_GET(self):
parts = urlsplit(self.path)
query = parse_qs(parts.query)
scenario = query.get("scenario", ["normal"])[0]
page = query.get("page", ["1"])[0]
if parts.path != "/v1/devices":
return self.send_json(404, {"error": "not-found"})
if scenario == "auth":
return self.send_json(401, {"error": "demo-auth-required"})
if scenario == "forbidden":
return self.send_json(403, {"error": "demo-permission-denied"})
if scenario == "throttle":
return self.send_json(429, {"error": "demo-throttled"}, {"Retry-After": "3"})
if scenario == "service":
return self.send_json(503, {"error": "demo-service-unavailable"})
if scenario == "bad-json":
return self.send_body(200, b"{not-json", "application/json")
if page == "1":
base = f"http://127.0.0.1:{self.server.server_port}"
payload = {"value": DEVICES[:2], "@odata.nextLink": base + "/v1/devices?page=2"}
elif page == "2":
payload = {"value": DEVICES[2:]}
else:
return self.send_json(404, {"error": "page-not-found"})
self.send_json(200, payload)
def send_json(self, status, payload, extra=None):
body = json.dumps(payload, sort_keys=True).encode("utf-8")
self.send_body(status, body, "application/json", extra)
def send_body(self, status, body, content_type, extra=None):
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
for key, value in (extra or {}).items():
self.send_header(key, value)
self.end_headers()
self.wfile.write(body)
def read_page(url, expected_port):
parts = urlsplit(url)
# HTTP is intentional ONLY for the local, fake server with no secrets.
if (parts.scheme != "http" or parts.hostname != "127.0.0.1"
or parts.port != expected_port or parts.username or parts.password
or parts.path != "/v1/devices"):
raise ValueError("Blocked unexpected continuation URL")
req = Request(url, headers={"Accept": "application/json"}, method="GET")
try:
with urlopen(req, timeout=4) as response:
status = response.status
body = response.read(64 * 1024 + 1)
if len(body) > 64 * 1024:
raise ValueError("Response exceeded 64 KiB limit")
except HTTPError as exc:
hint = exc.headers.get("Retry-After")
raise ValueError(f"HTTP {exc.code}" + (f"; Retry-After={hint}" if hint else "")) from None
except (URLError, TimeoutError) as exc:
raise ValueError("Network request failed or timed out") from exc
if status != 200:
raise ValueError(f"Unexpected HTTP {status}")
try:
data = json.loads(body.decode("utf-8"))
except (UnicodeError, json.JSONDecodeError) as exc:
raise ValueError("Invalid JSON response") from exc
if not isinstance(data, dict) or not isinstance(data.get("value"), list):
raise ValueError("Invalid API response shape")
return data
def collect(base_url, port):
seen = set()
hosts = []
url = base_url
for _ in range(5): # hard maximum, including the last page
if url in seen:
raise ValueError("Pagination loop")
seen.add(url)
page = read_page(url, port)
for item in page["value"]:
if (not isinstance(item, dict)
or not isinstance(item.get("host"), str)
or not item["host"].strip()
or item.get("status") not in ("online", "offline")):
raise ValueError("Invalid device record")
hosts.append({"host": item["host"].strip(), "status": item["status"]})
next_url = page.get("@odata.nextLink")
if not next_url:
return {"total": len(hosts),
"offline_hosts": sorted(x["host"] for x in hosts if x["status"] == "offline"),
"pages": len(seen)}
if not isinstance(next_url, str):
raise ValueError("Invalid pagination link")
url = next_url
raise ValueError("Page budget exceeded; no partial report allowed")
def main(argv=None):
parser = argparse.ArgumentParser(description="Fictional API over local loopback only")
parser.add_argument("--scenario", choices=SCENARIOS, default="normal")
args = parser.parse_args(argv)
server = ThreadingHTTPServer(("127.0.0.1", 0), DemoAPI)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
first = f"http://127.0.0.1:{server.server_port}/v1/devices"
if args.scenario != "normal":
first += "?scenario=" + args.scenario
report = collect(first, server.server_port)
print(json.dumps(report, sort_keys=True, indent=2))
return 0
except ValueError as exc:
print("Local API demo error: " + str(exc), file=sys.stderr)
return 1
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
if __name__ == "__main__":
raise SystemExit(main())Run and compare these commands
python local_api_demo.pypython local_api_demo.py --scenario authpython local_api_demo.py --scenario forbiddenpython local_api_demo.py --scenario throttlepython local_api_demo.py --scenario bad-jsonpython local_api_demo.py --helpUse the virtual environment interpreter from Lesson 01. No pip install is needed for this standard-library demonstration.
Expected evidence
The normal run produces a two-page report with three fictional devices and one offline host, AP-02. The authentication simulation should produce a clear HTTP 401 error and a nonzero exit status; the throttling simulation shows an HTTP 429 and a Retry-After: 3 hint. The example reports failures rather than automatically sleeping or retrying.
Read the read_page() function closely: it allows only the exact loopback host and port chosen by the local server. The all-or-nothing collect() loop validates every page before printing a result. A normal cloud API client will need an HTTPS origin allowlist, approved credentials, robust retry limits and auditing before being trusted with operational data.
Write API-handling logic yourself
Begin with an encoded read-only URL and work toward independent status handling, JSON schema checks, secret redaction, transport errors, rate limits, safe pagination URLs and a complete two-page inventory reporter. The workbench executes actual Python code and checks changed response fixtures without calling a live API.
Write it. Run it. Understand it.
Write real Python response-handling code with offline HTTP fixtures, safe pagination, token redaction and bounded error decisions. No network requests or credentials.
Build a properly encoded read-only URL
Implement make_devices_url(site), which builds a GET endpoint URL starting with https://api.example.invalid/v1/devices and exactly one query argument named site. Use urllib.parse.urlencode rather than manually concatenating the site string. Strip leading/trailing whitespace from site; return None for empty or non-string input. Print the URL for 'HQ & East'. Additional cases include spaces, ampersands and punctuation, and verify that parsing recovers the original site as one argument.
https://api.example.invalid/v1/devices?site=HQ+%26+EastTry solving it before asking for help. Hints never show a completed program.
Your printed output and errors will appear here.
Run the code, then step through the executed lines to watch variable values change.
Don't stop at the first 200 OK.
Use Check to verify your code handles 204, 401, 403, malformed data, unsafe pagination and timeouts. A valid API inventory requires every required page to succeed, not just the first request.
Continue to all eight API coding challenges →Next: Logging, Testing & Reliability →Applied Python path →Official references
The built-in Python modules and Microsoft Graph documentation underpin these examples. Each real service has its own supported endpoints, authentication requirements, paging contracts and rate limits; verify its current guidance before integrating a real environment.
