A Field Guide

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.

🧱 Part 1: Foundations
🔧 Part 2: Real-World Code
🎯 Part 3: Judgment

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.

💡 Tip
Plan for about 35 minutes. Each section builds on the previous one, but you can jump to any section that solves your current problem.

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.

Part One
Foundations
The core concepts that make code legible — what variables, functions, control flow, and data structures actually are and why they matter.

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.

📝 Example
"Sort by priority" as code requires you to define: What data structure is the list? What type is priority? What happens when two items have equal priority? What happens when the list is empty? What happens when priority is null?

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.

⚠️ AI Trap
AI models optimize for the common case, not the full case. They produce code that handles the input shape they expect — and quietly break on anything else. Everything in this guide builds on this insight.

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.

⚠️ Where AI Gets This Wrong
LLMs are very good at writing code that handles the type they were clearly told to expect. They're less reliable about handling type mismatches, particularly around null. Ask for a function that processes user input and you'll often get something that assumes the input is well-formed. In production, it won't be.
💡 The Null Question
When you're reviewing AI-generated code that takes any kind of external input — user data, API responses, file contents — your first question should be: what does this do when the input is null, or empty, or the wrong type? If there's no handling for those cases, the code is incomplete.

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.

💡 Single Responsibility
A function should do one thing. When a function starts doing multiple things — validating input and transforming data and writing to a database — it becomes hard to test, hard to reuse, and hard to understand when it breaks. This is one of the most common quality issues in AI-generated code: models produce functions that technically work but are doing three jobs at once.

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.

📝 Bad vs. Good
Bad — deeply nested:
def process_order(order):
    if order is not None:
        if order.status == "pending":
            if order.items:
                # ... 40 lines of actual logic
                return result
Good — guard clauses:
def 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 result

The 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.

⚠️ AI Pattern
AI often produces the nested version when given a vague specification. Models default to nesting conditionals rather than guard clauses because that pattern is more common in training data. Deeply nested conditionals in generated code are a quality signal worth flagging.

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.

📝 Array vs. Dictionary
Bad — scanning the whole list every time:
# 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 user
Good — instant lookup:
users = {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.

⚠️ Where AI Gets This Wrong
LLMs will use arrays where dictionaries are appropriate more often than experienced engineers will, especially when given a vague prompt. If a model hands you code that's "working" but doing repeated array scans, it's working correctly but inefficiently — and you won't notice until scale breaks it.
Part Two
Real-World Code
Errors, dependencies, scope, async, and APIs — the concepts that separate toy examples from production code.

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 Evaluation Posture
Syntax errors will be caught automatically. Runtime errors will surface during testing. But logic errors require you to reason about what the code should do and verify that it does that. When reviewing AI-generated code, this is where your attention should be.

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.

⚠️ Hallucinated APIs
When you ask a model to write code that uses libraries, it will write confident, syntactically correct code using library APIs that may have changed since its training data. It will use methods that have been deprecated, parameter names that were renamed, or features that didn't exist yet. The code looks right but targets the wrong version.

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 here

Variables 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.

⚠️ AI Async Pitfalls
AI-generated async code often has subtle issues: awaiting things that don't need to be awaited (making operations sequential that could be parallel), not awaiting things that should be awaited (producing code that continues before results are ready), or handling errors inconsistently. A common pattern: a try/catch that looks like it handles errors but doesn't actually catch async failures.

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.

Part Three
Judgment
Testing, patterns, honest limits, and how to work with AI on code productively.

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.

📝 Bad Test vs. Good Test
Bad — happy path only:
def test_calculate_discount():
    assert calculate_discount(100, 0.2) == 80
This test passes even if the function is hardcoded to return 80.

Good — 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%
💡 Tip
When you ask AI to generate tests, explicitly ask for edge cases and error cases. The model defaults to happy-path testing because that's where most training examples live. AI-generated tests that only test happy paths are arguably worse than no tests — they create false confidence.

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.

💡 Quality Signals
Code that uses guard clauses instead of nested conditionals, separates mappers from business logic, and uses factories for object creation — that code is structured with thought. Code that ignores all of these and just gets the job done is technically correct but will be a problem when requirements change.

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

✅ Before You Ship AI-Generated Code

☐ 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.

🚀 Next Steps
The Reading Code guide covers how to trace unfamiliar logic. The Debugging guide covers what to do when you find the gaps. The Security guide covers the failure modes that adversaries exploit. The APIs guide covers the mechanics of working with external services. The SQL guide covers how databases think about data.