Coding Basics
for AI
A practical guide for people who work with code — especially AI-generated code — and need to know when to trust it and when to push back.
Who This Is For
This guide is for people who find themselves increasingly adjacent to code — reviewing AI-generated output, giving direction to engineering teammates, writing prompts that produce functions, or trying to evaluate whether what an LLM just handed them actually works.
You don't need to be a programmer. You need to understand how programmers think, what makes code good or broken, and — critically — where AI-generated code tends to quietly fail you.
By the end, you'll have a mental model that makes code legible where it used to feel opaque, a working vocabulary for the most common patterns you'll encounter, and enough judgment to know when to trust what a model gives you versus when to push back or escalate.
Why This Matters Now
Something shifted in 2023. Before, "you don't need to learn to code" was defensible advice for knowledge workers — there were specialists for that. Now, LLMs produce competent-looking code on demand, which means the bottleneck moved. It's no longer generating code. It's evaluating it.
That's a different skill set, and it's one most people haven't deliberately built.
If you're managing people who write code, specifying systems, reviewing AI output before shipping it, or building lightweight automations yourself, you are now a participant in the code-production process whether you identify as a developer or not. The question isn't whether to develop some coding literacy. It's whether to do it intentionally or by osmosis.
This guide takes the intentional approach. It focuses on the conceptual vocabulary that makes code readable to a non-expert, the failure patterns that catch people off guard, and the specific ways AI makes this both easier and more treacherous than it used to be.
The Mental Model: Code Is a Specification
Here's the core insight that separates people who struggle with code from people who can navigate it fluently: code isn't instructions for a human reader. It's a formal specification for a machine that will execute it literally.
This sounds obvious until you feel the implications. When you tell a colleague to "sort the list by priority," they use judgment — they know roughly what you mean, they'll handle edge cases, they'll ask if something seems off. When you write that instruction as code, there is no judgment. The machine executes exactly what you specified, including your mistakes.
The person who struggles with code keeps expecting the machine to understand. The person who's fluent has internalized that code is always literally interpreted, and they've started writing — and reading — with that in mind.
This shifts how you read unfamiliar code. Instead of asking "what does this do," you start asking "what exactly does this do, in every case." It also shifts how you evaluate AI-generated code: the model will give you something that looks intentional and handles the happy path beautifully, while silently failing on any input that doesn't match the shape it was trained to expect.
Variables, Types, and Why the Machine Needs Both
A variable is a named container for a value. That's the simple version. The part that matters is that variables have types — they hold a specific kind of thing, and the machine behaves differently depending on what kind.
The Types to Recognize
String — text. "hello", "42", "true". Note that "42" is a string of two characters, not a number.
Integer — whole numbers. 42, -7, 0.
Float — decimal numbers. 3.14, -0.5.
Boolean — exactly true or false.
Null / None / undefined — the explicit absence of a value. This is different from zero, from empty string, from false. Null means nothing was provided.
Array / List — an ordered collection of values. [1, 2, 3].
Object / Dictionary / Map — a collection of key-value pairs. {"name": "Alice", "age": 34}.
Why Types Matter
Types matter because operations that work on one type often fail silently or produce nonsense on another. Adding two numbers gives you arithmetic. Adding a number and a string in many languages gives you string concatenation, or an error, depending on the language. Comparing 0 to false often returns true even though they're different types. Trying to do arithmetic on null crashes.
Functions: The Unit of Reusable Intention
A function is a named block of code that does one specific thing, takes some inputs, and returns some output. Functions are how programmers avoid repeating themselves and how they create abstractions — giving a name to a concept so you can use it without remembering how it works internally.
Anatomy of a Function
A function has a name (what you call it), parameters (the inputs it accepts), a body (what it does), and a return value (what it hands back).
def calculate_discount(price, discount_rate):
if discount_rate < 0 or discount_rate > 1:
raise ValueError("discount_rate must be between 0 and 1")
return price * (1 - discount_rate)This function takes a price and a discount rate (expected as a decimal between 0 and 1), validates the input, and returns the discounted price. Notice the guard clause on the input — that's what makes this function robust rather than fragile.
Naming as Documentation
A well-named function acts as documentation. calculate_discount(price, discount_rate) tells you what it does without reading the body. A function named process_data tells you almost nothing. When you're reading AI-generated code, poor function names are often a signal that the model wasn't given a clear specification — it named things generically because it didn't know what specific thing was being done.
Control Flow: How Code Makes Decisions
Control flow is how code decides what to do next. It's the difference between code that always does the same thing and code that responds to its inputs.
Conditionals
"If this condition is true, do X. Otherwise, do Y."
if user.is_premium:
show_advanced_features()
else:
show_upgrade_prompt()Loops
"Repeat this block of code for each item in a collection."
for order in pending_orders:
send_confirmation_email(order)Guard Clauses: Fail Fast
Early returns that exit a function when a precondition fails. This pattern dramatically affects readability and is a genuine quality signal in code.
def process_order(order):
if order is not None:
if order.status == "pending":
if order.items:
# ... 40 lines of actual logic
return resultdef process_order(order):
if order is None:
return None
if order.status != "pending":
return None
if not order.items:
return None
# ... 40 lines of actual logic, no nesting
return resultThe bad version nests every condition inside the previous one. As complexity grows, you end up with logic buried six levels deep that's nearly unreadable. The good version handles failure cases immediately and gets them out of the way — this is called "failing fast," and it's a genuine quality signal.
Data Structures: How Information Is Organized
Data structures are how code organizes information, and the choice of data structure determines almost everything about how code operates on that information.
Arrays / Lists
Ordered sequences. [1, 2, 3]. You can add to them, iterate over them, sort them, filter them. They're efficient when you need to work with things in sequence. They're slow when you need to check whether something is in them, because you have to look at every element.
Dictionaries / Maps / Objects
Key-value stores. {"user_id": 123, "name": "Alice"}. You give them a key and get back the associated value instantly. They're the right choice when you need to look things up by some identifier.
Sets
Like arrays but without duplicates. Efficient for membership testing — "is this item in the collection?"
The Wrong Data Structure
The choice between these isn't just stylistic — it affects what operations are natural to express, and at scale, it affects performance enormously.
# Finding a user by ID requires scanning the whole list
users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
for user in users:
if user["id"] == target_id:
return userusers = {1: {"id": 1, "name": "Alice"}, 2: {"id": 2, "name": "Bob"}}
return users.get(target_id)The array version scans every user on every lookup. If you have ten thousand users and do ten thousand lookups, that's one hundred million operations. The dictionary version does ten thousand.
The Error Model: What Breaks and Why
Code breaks in three ways that are worth keeping distinct in your mind.
Syntax Errors
The code equivalent of a grammatical mistake — the code is so malformed the machine won't even attempt to run it. Missing parenthesis, misspelled keyword, wrong indentation. These fail loudly and immediately. AI almost never produces syntax errors.
Runtime Errors
Failures that only occur when the code runs. Dividing by zero. Calling a method on null. Trying to open a file that doesn't exist. These fail at a specific moment, with a specific error message pointing at a specific line. They're diagnosable. AI produces these occasionally, particularly around null handling and type mismatches.
Logic Errors
The dangerous ones. The code runs without errors and produces the wrong answer. The discount function that returns the discount amount instead of the discounted price. The sort that's ascending when it should be descending. The date calculation that's off by one day because timezone wasn't accounted for. These fail silently.
The Off-by-One Error
This deserves its own mention because it's the most common logic error and it comes up constantly. When code iterates from 0 to n, does it include n? When you're slicing an array from index 2 to 5, do you get elements at indices 2, 3, 4, or 2, 3, 4, 5? These edge cases — the first element, the last element, the empty collection — account for a disproportionate percentage of bugs in otherwise correct code, and AI generates them with the same cheerful confidence it generates everything else.
Dependencies and the Import Model
Almost no code is written from scratch. Modern code relies heavily on libraries — packages of code written by others that you incorporate into your program.
How Imports Work
The act of using a library starts with importing it:
import pandas as pd from datetime import datetime
You declare what external code you're depending on, and the runtime loads it. Libraries handle things like HTTP requests, data manipulation, date handling, authentication — the kind of code that every program needs and no one should write themselves.
The Dependency Graph
Your code depends on library A, which depends on library B, which depends on library C. When something breaks in C, it can propagate upward and break your code even though your code didn't change.
Dependency management is the practice of specifying exactly which versions of libraries you depend on, so that the program that works today still works tomorrow. This is what files like package.json, requirements.txt, and Gemfile are for. They pin the dependency tree.
The symptom: you copy the generated code, run it, and get an error like "has no attribute X" or "unexpected keyword argument." Check the actual library version you're running and verify the API exists as written.
The Scope Problem
Scope determines where a variable can be seen. It seems simple until it bites you.
x = 10 # global scope — visible everywhere
def my_function():
y = 20 # local scope — only visible inside this function
print(x) # works — can see global variables
print(y) # works — local variable
print(x) # works
print(y) # error — y doesn't exist out hereVariables defined inside a function exist only inside that function. When the function returns, those variables are gone. This is by design — it lets you write functions without worrying about accidentally using the same variable name somewhere else in the program.
Global State
The trap is global state: variables defined at the top level that can be modified by any function. Code that relies heavily on global state is hard to reason about because you can't understand what a function does without also understanding all the global state it might read or modify. This pattern comes up in AI-generated code when the model is asked to build something incrementally and starts stashing state in globals rather than passing it explicitly between functions.
Closures
A function that "captures" variables from the scope where it was defined, even after that scope is gone. When a function is created inside another function, it keeps a reference to the outer function's variables. This is intentional and useful — but it can cause subtle bugs if you expect those captured variables to have the value they had when you looked at them, not the value they have when the function actually runs.
Asynchronous Code: When Things Don't Happen in Order
Most code you'll encounter in web development and API work is asynchronous — it starts an operation and continues without waiting for it to finish, picking up the result later. This exists because waiting is expensive.
Async / Await
The modern syntax for asynchronous code. An async function can pause at any await expression until some operation completes, without blocking everything else.
async function fetchUserData(userId) {
const user = await database.getUser(userId);
const orders = await database.getOrders(userId);
return { user, orders };
}Promises
The underlying mechanism — an object representing a value that isn't available yet. await is syntactic sugar that makes working with promises look like synchronous code.
Race Conditions
When you have multiple async operations running concurrently and the result depends on which one finishes first, you have a potential race condition. These are notoriously hard to reproduce and debug because they're timing-dependent.
APIs and the Interface Contract
An API is a contract between a caller and a provider. The contract says: if you send me this input in this format, I will return this output in this format, and these are the ways it can fail. For a deeper dive, see the APIs field guide.
Handle Every Case
Your code should explicitly handle every case the contract describes, not just the happy path. An API that can return a 200, a 404, a 429 (rate limited), or a 500 requires code that handles all of those. AI will frequently generate code that handles 200 and maybe 400, assumes 429 is unlikely, and crashes on 500.
Schema Validation
Verifying that data matches the expected structure before you try to use it. An API response claims to have a user.profile.email field. Your code should check that user exists, that profile exists inside it, and that email exists inside that — before trying to use the email. The alternative is TypeError: Cannot read property 'email' of undefined.
Idempotency
An idempotent call can be made multiple times with the same result. Getting a resource is naturally idempotent. Creating a resource often isn't — calling it twice creates two resources. This matters when you're writing retry logic: if your code retries a failed call, you need to know whether retrying could cause duplicate side effects.
Testing: The Specification You Write After the Fact
Tests are code that calls your code and verifies it behaves correctly. They're the executable specification — a precise, runnable statement of what the code is supposed to do.
The Hierarchy
Unit tests test individual functions in isolation. They're fast, specific, and tell you exactly what broke.
Integration tests test multiple components working together — your code plus a real database, or two services communicating. Slower and more realistic.
End-to-end tests simulate a real user interaction. Slow, fragile, but the only way to verify the whole system works together.
Good Tests vs. Bad Tests
When a model gives you a function, the most useful thing you can do is ask it to also write tests. Then read the tests carefully.
def test_calculate_discount():
assert calculate_discount(100, 0.2) == 80Good — edge cases and error cases:
def test_calculate_discount():
assert calculate_discount(100, 0.2) == 80 # happy path
assert calculate_discount(0, 0.5) == 0 # zero price
assert calculate_discount(100, 0) == 100 # zero discount
assert calculate_discount(100, 1.0) == 0 # full discount
with pytest.raises(ValueError):
calculate_discount(100, -0.1) # invalid: negative
with pytest.raises(ValueError):
calculate_discount(100, 1.5) # invalid: over 100%Common Patterns: The Named Moves
Experienced programmers recognize recurring code structures the way chess players recognize openings. Giving these patterns names lets you think and communicate at a higher level.
Guard Clause
An early return at the top of a function that handles invalid or edge-case inputs before the main logic runs. A sign of defensive, readable code.
Null Object Pattern
Instead of returning null when something isn't found, return an empty version of the expected type. Instead of returning null for a missing user, return a GuestUser object that implements the same interface but represents an unauthenticated state. Eliminates null checks throughout the codebase.
Mapper / Transformer
A function whose sole job is converting data from one shape to another. "Take this API response and turn it into this internal model." Keeping this separate from business logic is a mark of good architecture.
Reducer
A function that takes a collection and a starting value, and repeatedly applies some operation to fold the collection into a single result. Summing a list of numbers is a reduce. The pattern: (accumulator, currentItem) => newAccumulator.
Factory
A function or object whose job is creating other objects. Instead of directly instantiating a database connection, you call a connection factory that handles configuration, pooling, and retry logic.
Strategy Pattern
Parameterizing a behavior by passing in a function that implements it. Instead of hard-coding a sorting algorithm, you accept a comparison function: array.sort((a, b) => a.priority - b.priority). The strategy is passed in rather than built in.
Memoization
Caching the result of an expensive function call so that subsequent calls with the same input return the cached result. Straightforward to implement, enormous performance impact for the right use cases.
Where This Breaks Down
There are things this guide covers shallowly or not at all, and some of them are genuinely important. Here's the honest assessment.
Concurrency at scale is not handled here. The section on async code gives you vocabulary, but real concurrency — multiple threads or processes sharing state — is a different problem with different failure modes.
Performance and complexity analysis. Knowing that an array scan is slower than a dictionary lookup is useful. Understanding why in terms of computational complexity — Big O notation, cache behavior, memory allocation — requires more depth. If you're evaluating code that will run at scale, see the Performance field guide.
Language-specific idioms. This guide works at the level of concepts that cross language boundaries, but "is this good code" is partly a question of whether it uses the language's conventions.
Security. Security vulnerabilities are a specific class of logic error where the "wrong case" is one that an adversary deliberately constructs. AI generates vulnerable code with some regularity, and it does so with the same confident tone it uses for everything else. See the Security field guide.
The limit of conceptual understanding. There's a threshold past which you need to actually write and debug code to develop real fluency. This guide builds the foundation for reading code. Writing and debugging require practice that no guide substitutes for. See the Reading Code and Debugging field guides for the next steps.
Using AI Well for Coding
The preceding sections have noted specific failure patterns. Here's the consolidated view of how to work with AI on code productively.
Lead with Context
"Write a function to process user orders" produces a generic implementation. "Write a function to process user orders in an e-commerce system where orders can have mixed payment methods and the function will be called asynchronously during checkout" produces something much more relevant. Anchor the AI on the actual use case, not just the technical spec.
Ask for the Unhappy Paths
After getting an implementation you like, ask: "What input would make this function fail, and does the code handle those cases?" Models are better at finding their own edge cases when asked to look than at proactively covering them.
Use AI to Explain, Not Just Generate
Paste unfamiliar code and ask "walk me through what this does and what assumptions it makes." Models are excellent at code explanation — often more useful for this than any other application.
Treat Generated Code as a First Draft
The model will rarely give you production-ready code on the first pass. It gives you a working shape that you then harden — adding error handling, removing magic values, adding tests, naming things properly. Expecting the first pass to be deployable sets you up for quiet failures.
Watch for Hallucinated APIs
If generated code imports a library or calls a method, verify that import and method exist in the version you're running. The model's knowledge of library internals is often accurate but not always current.
Don't Trust AI for Security-Sensitive Code
Input validation, authentication, authorization, cryptography — these are the places where AI-generated code has a higher error rate in categories that matter most. Not because the model doesn't know the concepts, but because the failure modes require adversarial thinking that models don't reliably apply.
Generate Tests After Code
Ask the model to write tests for the code it just wrote. Then review whether those tests actually test the interesting cases, or just the happy path. AI-generated tests that only test happy paths create false confidence.
Quick Reference
A cheat sheet for the concepts covered in this guide.
Types
String — text. "hello", "42"
Integer — whole number. 42, -7
Float — decimal. 3.14
Boolean — true or false
Null/None — explicit absence of a value
Array — ordered list. [1, 2, 3]
Object — key-value pairs. {"key": "value"}Control Flow Patterns
if/else — conditional branching for/while — repetition guard clause — early return on failure try/catch/except — error handling
Scope
global scope — visible everywhere local scope — visible only inside function closure — function captures outer variables
Async Vocabulary
async function — can pause without blocking await — pause until this operation completes Promise — future value not yet available
Error Types
Syntax error — malformed code, fails before running Runtime error — fails during execution, with traceback Logic error — runs fine, wrong answer (the dangerous one)
Common Patterns
Guard clause — fail fast at function entry Null object — return empty object instead of null Mapper — convert data from one shape to another Reducer — fold collection into single value Factory — centralize object creation Strategy — pass in the behavior as a function Memoization — cache expensive function results
AI Code Review Checklist
☐ What happens when inputs are null or wrong type?
☐ What happens on the error paths from any API calls?
☐ Do tests cover edge cases, not just happy path?
☐ Do library imports actually exist in the version I'm running?
☐ Are functions doing one thing, or three things at once?
☐ Is there deep nesting where guard clauses should be?
What You Can Do Now
The single most useful thing you can do right now is find a piece of AI-generated code and apply the null question.
Find a piece of AI-generated code — from a recent project, from a colleague, generated fresh for this exercise — and for each input it accepts, ask: what happens when that input is null, empty, or the wrong type?
Follow the code's logic for each case. If you find a case where the code continues without handling it, that's a real gap. If the code handles it, notice how: does it validate at the top with a guard clause, or check inside nested conditionals? Does it raise an explicit error or silently return a confusing result?
This single exercise — applied once, to real code — will teach you more about what robust code looks like than re-reading this guide twice. The conceptual shift from "does this code do the thing" to "does this code do the thing in every case" is the one that sticks when you practice it on actual code.