You don't call the framework — it calls you. The distinction that changes how you read documentation, with examples in JavaScript and Python.

Every week someone asks me some version of this:
Is Express a framework or a library?
Is the OpenAI SDK a framework?
Should I learn frameworks or SDKs first?
It sounds like a beginner question, but the confusion sticks around well into year three of a career. And it is not for lack of studying — it is that almost nobody explains the mechanism behind either one. People memorize names instead.
Once you understand the mechanism, something useful happens: you read docs faster, debug with less guesswork, and pick tools without depending on a Twitter thread. Let us get into it.

Hold on to this sentence, because it settles about 80% of the confusion:
You call the library. The framework calls you. The SDK talks to the outside world.
It sounds reductive. It is not. It is literally about your program''s flow of control — who is holding the steering wheel during execution.

A library is a bag of ready-made functions. You import it, call it when you feel like it, and execution comes right back to you.
import { format } from 'date-fns';const today = format(new Date(), 'MM/dd/yyyy');console.log(today); // your code is still drivingfrom datetime import datetimetoday = datetime.now().strftime('%m/%d/%Y')print(today) # your code is still drivingYou are the director. The library is the actor who steps in when you yell action. If you never call it, it does nothing.

Here everything flips. With a framework, you do not call its code — it calls yours.
This has a name: Inversion of Control (IoC), also known as the Hollywood Principle: do not call us, we will call you.
import express from 'express';const app = express();// You are NOT executing anything here.// You are REGISTERING a function and saying:// "when a GET hits /users, call this"app.get('/users', (req, res) => { res.json([{ id: 1, name: 'Ana' }]);});app.listen(3000); // from this line on, the framework is in controlfrom fastapi import FastAPIapp = FastAPI()# Same idea: the decorator registers your function with the framework.# It decides when the function runs, not you.@app.get("/users")def list_users(): return [{"id": 1, "name": "Ana"}]Notice what you did not write in either snippet:
you did not open a TCP socket
you did not parse raw HTTP text
you did not build the response headers
you did not write the infinite loop waiting for connections
you did not handle concurrent requests
All of that exists and is running right now. It just lives inside the framework.

Every framework — web, UI, testing, whatever — runs on the same three-beat skeleton.
1. Registration phase. You declare your pieces: routes, components, middleware, jobs, test cases. The framework stores them in an internal structure. app.get(...) does not execute your function; it pushes a record onto a list.
2. Bootstrap phase. You hand over control (app.listen(), uvicorn main:app, npm test). The framework assembles what it needs: server, dependency injection, connections, component tree.
3. Execution loop. The framework runs forever, listening for events. When one arrives, it consults that list from step 1, finds your function, and calls it — handing you pre-chewed data (req, res, props, event).
Here is crude pseudocode of what Express is doing while you sleep:
// this is the framework, not your codewhile (true) { const rawRequest = await socket.acceptConnection(); const req = parseHttp(rawRequest); // becomes an object const route = routingTable.find(req.method, req.url); if (!route) return respond404(); for (const middleware of middlewares) { // lifecycle await middleware(req, res); } await route.handler(req, res); // <- YOUR code, at last}This is why frameworks have a lifecycle (useEffect, beforeEach, middleware, onMount). Those are hooks the framework offers so you can inject code at specific moments of the loop it controls. You do not get to choose when your code runs — only where it hangs.
It is also why frameworks are opinionated. By letting one drive, you accept its folder structure, its naming, its way of doing things. In exchange you get speed, and you do not rewrite HTTP parsing for the thousandth time in the history of computing.

SDK stands for Software Development Kit. In practice, today, nearly every SDK is the same thing: a package that wraps a service''s API so you do not have to speak raw HTTP.
Without an SDK, integrating with a service looks like this:
import uuidimport requestsresponse = requests.post( "https://api.example.com/v1/charges", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Idempotency-Key": str(uuid.uuid4()), }, json={"amount": 5000, "currency": "usd"}, timeout=30,)if response.status_code == 429: ... # now what? backoff? how many retries?if response.status_code >= 500: ... # retry? did the charge already go through?data = response.json() # untyped dict, no autocompleteWith an SDK:
from stripe import StripeClientclient = StripeClient(api_key=api_key)charge = client.payment_intents.create({ "amount": 5000, "currency": "usd",})print(charge.id)import Stripe from 'stripe';const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);const charge = await stripe.paymentIntents.create({ amount: 5000, currency: 'usd',});console.log(charge.id);Three header lines and all the error handling vanished. But they did not stop existing — they just moved.

Authentication. Builds the header, refreshes expired tokens, reads the environment variable.
Serialization. Turns objects into JSON and JSON back into typed objects. You get autocomplete, which is underrated until you lose it.
Retry with exponential backoff. Got a 429 or a 503? Once you enable it, the client retries quickly on the first failure and then on an exponential backoff schedule, instead of giving up. Note it is opt-in: Stripe ships the mechanism, you configure it.
Idempotency. Generates a unique key so a retry does not charge your customer twice — Stripe stores that key for 24 hours and replays the original result. This one item alone pays for the SDK.
Pagination. Turns cursor juggling into a plain iterator.
Typed errors. RateLimitError, AuthenticationError — instead of comparing loose status numbers.
Versioning. Pins the remote API version so the server does not shift under your feet.
None of this is magic. It is boring, tedious, edge-case-riddled code that somebody already wrote, battle-tested in production, and maintains for you. That is the SDK.

Trait | Library | Framework | SDK |
|---|---|---|---|
Who calls whom | you call it | it calls you | you call it |
Flow of control | yours | its | yours |
Problem it solves | one specific task | your app structure | talking to an external system |
Where the work runs | your machine | your machine | someone else''s server |
How many per project | dozens | 1 (rarely 2) | one per service |
Cost of switching | low | brutal (rewrite) | medium |
Examples | lodash, date-fns, pandas | React, Django, Rails, Spring | Stripe, AWS boto3, OpenAI |
The most practical row is the cost of switching. Swapping date-fns for day.js is an afternoon. Swapping Django for FastAPI is a quarter. That is why framework choices deserve a meeting and library choices do not.

And there is nothing wrong with that:
import osfrom fastapi import FastAPI # framework: runs the applicationfrom stripe import StripeClient # SDK: talks to Stripefrom datetime import datetime # library: one specific taskapp = FastAPI()stripe = StripeClient(api_key=os.environ["STRIPE_KEY"])@app.post("/subscribe") # the FRAMEWORK will call thisdef subscribe(amount: int): charge = stripe.payment_intents.create({ # YOU call the SDK "amount": amount, "currency": "usd", }) return { "id": charge.id, "created_at": datetime.now().isoformat(), # YOU call the library }Three import lines, three different roles. The framework is the stage. The SDK is the phone. The library is the tool on the workbench.

You debug better. A bug in framework code is almost never the framework''s fault — it is you using a hook at the wrong moment of the lifecycle. A bug in SDK code is almost always network, credentials, or rate limits. Knowing that cuts your investigation time in half.
You read docs in the right order. Framework docs: start with the lifecycle and the folder structure. SDK docs: start with auth and the error codes. Library docs: go straight to the function signature.
You choose with actual criteria. A framework is a marriage — check the community, the maintenance, the release cadence. An SDK is a vendor — prefer the official one, verify it has retries built in, and see how long since the last commit.

Facing any unfamiliar package, ask one question:
Does my code call this, or will this call my code?
That is it. Nothing else to memorize.
If this was useful, the natural next step is to open the source of whatever framework you use most and go find the main loop. Seeing with your own eyes the exact moment it calls your function is one of those things you cannot unsee — and it is what separates people who use tools from people who understand them.
Inversion of Control — Martin Fowler — the canonical definition. Fowler puts it plainly: the control is inverted, it calls me rather than me calling the framework. Also where the Hollywood Principle is tied to the concept.
Idempotent requests — Stripe API Reference — official documentation on idempotency keys: V4 UUIDs recommended, up to 255 characters, pruned after 24 hours.
Advanced error handling — Stripe Documentation — how the official libraries retry: first attempt quickly, then exponential backoff, respecting the Stripe-Should-Retry header. Confirms retries are opt-in and must be configured.
The framework definition Fowler cites comes from Ralph Johnson and Brian Foote: the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user''s application code. That sentence is from 1988 — the idea in this article is older than most of the tools it uses as examples.