Skip to main content
Learning Center
Python Programming

Module 13: AI-Assisted Software Development

Invented APIs, Stale Advice, and What Never Gets Pasted

The failure modes that come with generated code, why verification has to be mechanical rather than intuitive, and the categories of data that never enter a prompt at any size.

Lesson 42 of 46 in the recommended order · About 25 min (estimate)

On this page

Outcome

By the end of this lesson you can check a generated claim about a library or an API in a way that does not depend on the claim sounding right, and you can state exactly what never goes into a prompt regardless of how small it looks.

Why it matters

The failure modes in this lesson share one property: the output looks correct. A method that does not exist is named the way a real method would be. Advice about a library reflects a version that was current a while ago. A snippet is convenient to paste and happens to include a line you did not read.

None of these is caught by careful reading, because careful reading is what produces the impression that it is fine. They are caught by mechanical checks, and the checks are short.

Concept

Invented APIs. A generated call to a method that does not exist is the single most common failure, and it is well-named, plausibly argued, and confidently explained. Two checks:

  • Run it. An AttributeError or a TypeError on the signature settles it in seconds.
  • Look it up in the library's own current documentation, not in a summary of it.

dir(obj) and help(obj.method) in a Python session answer "does this exist and what does it take" directly, which is faster than reading an explanation of why it should.

Stale assumptions. An assistant's knowledge has a date, and libraries move. Deprecated arguments, renamed functions, changed defaults, and superseded security advice all read exactly like current information. Anything version-sensitive, and API details and security guidance especially, is worth confirming against the current official source before it goes in.

Insecure defaults. Generated code has recurring habits this course has already met: SQL built by concatenation, except Exception: pass, credentials in URLs, while True around a network call, verify=False on a request, secrets logged inside a configuration object. Each of them is the shortest way to write the thing. Check for them by name.

Dependencies. A suggested package is a decision: it needs a maintainer, a licence, a security history, and a reason. A generated import can also name a package that does not exist, which is a supply-chain risk in its own right, because someone may later publish something under that name. Confirm a package exists and is the one you meant before installing it.

Data privacy. These never enter a prompt, at any size, in any tool:

  • Credentials, API keys, tokens, passwords, connection strings.
  • Personal data about real people.
  • Client, employer, or agency data you are not free to publish.
  • Controlled or restricted procurement information.
  • Internal hostnames, file paths, and infrastructure detail.
  • Full tracebacks or configuration files without reading them first.

The working rule: reproduce the shape with invented values. Structure is what a technical question is about, and structure survives replacing every value. Two synthetic records explain a parsing bug exactly as well as two real ones.

When a snippet genuinely must be shared, run a redaction pass over it first, matching on the names of settings rather than on the shape of their values. A credential can look like anything; the settings that hold one are named predictably.

Finally, scope. This course does not require any paid AI account, and the capstone is completable without ever using an assistant. These are tools with failure modes worth knowing, not a dependency of the curriculum.

Read the code

import json

CANDIDATE = """
data = json.parse(payload)
records = data.get_list("results")
"""

MARKERS = ("key", "token", "secret", "password")


def exists(module, name):
    """Mechanical check: does this attribute actually exist?"""
    return hasattr(module, name)


def redact(text):
    """Mask the value of any credential-shaped setting, keep the names."""
    lines = []
    for line in text.splitlines():
        name, separator, _ = line.partition(":")
        if separator and any(marker in name.lower() for marker in MARKERS):
            lines.append(f"{name}: REDACTED")
        else:
            lines.append(line)
    return "\n".join(lines)


print(exists(json, "parse"), exists(json, "loads"))
print(sorted(n for n in dir(json) if not n.startswith("_"))[:5])
print(redact("state: OR\ndata_source_key: sample-value-not-real\noutput: shortlist.csv"))

CANDIDATE is generated code of the kind that reads perfectly well. json.parse is what the equivalent is called in JavaScript, and a dictionary has no get_list method. Both are plausible and neither exists.

exists is the whole verification technique: hasattr answers the question the code is implicitly claiming. dir(json) lists what is actually there, which turns "is this the right name" into a lookup rather than a recollection.

redact matches on the setting name, so it does not need to recognise what a credential looks like. Keeping the name and removing the value is what leaves the snippet useful.

Predict the output

Predict the three printed lines. The middle one lists the first five public names in the json module, alphabetically.

Check your prediction
False True
['JSONDecodeError', 'JSONDecoder', 'JSONEncoder', 'codecs', 'decoder']
state: OR
data_source_key: REDACTED
output: shortlist.csv

json.parse does not exist and json.loads does. That is the entire check, and it took one line.

The redaction kept both ordinary settings and both setting names, and removed only the value. Someone helping you can still see that the credential is set and which one it is, which is almost always what the question actually needs.

Modify the code

Change MARKERS to ("KEY", "TOKEN") in upper case, leaving name.lower() in place. Predict what the redaction does now.

What changes, and why

Nothing is redacted. The credential line passes through with its value intact.

name.lower() produces data_source_key, and "KEY" in "data_source_key" is False, because in on strings is case-sensitive. One side was normalised and the other was not, which is Module 6's half-done normalisation appearing in a security control.

Two lessons. A redaction that silently does nothing is worse than none, because it creates confidence without protection. And a control like this needs a test with a credential-shaped line asserting the value is gone, exactly as any other behaviour would.

Debug the bug

An assistant was asked how to fetch data from an API when the request was failing with a certificate error. It replied with this and an explanation that the setting disables certificate checking so the request can proceed.

import requests

response = requests.get(
    "https://example-procurement.test/search",
    params={"state": "OR", "access_token": "sample-value-not-real"},
    verify=False,
    timeout=None,
)
print(response.json())
What's actually wrong

The explanation is accurate and the advice is wrong, which is the most difficult combination to catch.

verify=False turns off certificate verification. The certificate error was the connection reporting that it could not confirm who it was talking to. Disabling the check does not fix that; it removes the warning and proceeds, which is precisely the condition an interception attack needs. The real causes are usually a corporate proxy, an out-of-date certificate bundle, or a clock that is wrong, and all three have honest fixes.

A credential value is written into the source. Wherever this file goes, the value goes: into the repository, into every clone, into the diff, into the assistant's context window. That fault is independent of how the value was going to be sent.

timeout=None means wait forever. A request with no timeout can hang a scheduled job indefinitely with no error to alert anyone.

No status check. response.json() on an error response raises a decoding error rather than reporting the status, so the failure arrives disguised.

A note on the parameter, because this is where generated code and confident advice both tend to overreach. Whether a credential travels in a header, in a query parameter, or in a signature is decided by the provider and stated in its documentation. There is no universal rule, so "put it in a header" is not a correction anyone can make without having read the contract, and neither is "never put it in a URL". What is always true is that the value does not belong in source, in a log, in a screenshot, or in a message to an assistant.

The corrected form, with no credential involved because this project needs none:

import requests

response = requests.get(
    "https://example-procurement.test/search",
    params={"state": "OR"},
    timeout=10,
)
if response.status_code != 200:
    raise RuntimeError(f"search failed with status {response.status_code}")

Verification left on, a real timeout, and the status checked before the body is parsed. If the certificate error persists, that is a genuine problem to investigate rather than to switch off. And note what is not here: this course's own client never calls requests at all, because it takes its transport as an argument, which is what lets every one of these failure paths be tested from a stored response.

Try it yourself

Write the redaction pass that runs before a configuration snippet is shared. Two settings are credentials and two are not.

Loading this exercise…

Practical challenge (optional)

Optional: write your own one-page list of what you will never paste, adapted to your actual situation, employer, client, or personal. Then take a real error you have hit recently and produce a shareable version of it: the same structure, every value invented. Compare the two and check the second still asks the same question. Doing this once makes the habit fast, and the habit is what protects you when you are tired and the answer feels close.

Sign in to track your progress on this exercise.

AI collaboration

Checkpoint

  1. What is the fastest way to check whether a generated method call exists?
  2. Why is stale advice about a library harder to spot than an invented method?
  3. Name four categories of information that never go into a prompt.
  4. Why match a redaction on setting names rather than on value shapes?
Answers
  1. Run it, or check with hasattr and dir. An AttributeError settles it immediately, and no amount of plausibility outweighs that.
  2. An invented method fails when you run it. Stale advice runs, produces output, and is simply wrong about a default, a deprecation, or a security recommendation, so nothing draws attention to it.
  3. Credentials; personal data about real people; client, employer, or agency data you are not free to publish; controlled procurement information; internal hostnames and paths; unread tracebacks or configuration files.
  4. A credential can take any form, so matching its shape is unreliable. The settings that hold credentials are named predictably, so matching the name catches values you would not recognise.

Sign in to track your progress on this exercise.

Summary and next step

Check existence mechanically rather than by plausibility, confirm anything version-sensitive against the current official source, look for the recurring insecure defaults by name, treat a dependency as a decision, and redact by name before sharing anything at all. Module 14 brings every thread together into the finished Opportunity Review Assistant.

learning.goultergroup.com

The interactive parts of this page have not loaded. Reading and links still work; reload the page to try again.