Intuition Why a "scenario matrix" first?
When you meet a real problem, the hardest step is recognising which kind of problem it is. SymPy can solve, diff, integrate, dsolve... but which one, and what traps hide in each? This page lays out a matrix — a grid of every case-class the parent note can throw at you — then works one example per cell. By the end there is no scenario you have not seen answered.
A "case-class" here means: a family of inputs that behave the same way (e.g. "quadratic with real roots" vs "quadratic with complex roots"). Different classes need different handling, so we treat each on its own.
This page assumes the tools from the parent SymPy note . If a symbol here feels unfamiliar, it is defined below before use.
Read each row as "a situation this topic creates" and the cell as "the twist that makes it its own case."
#
Category
The specific case (the twist)
Worked in
A
Solving
Quadratic with two real roots
Ex 1
B
Solving
Quadratic with complex roots (discriminant < 0)
Ex 2
C
Solving
Degenerate equation: no solution / infinitely many
Ex 3
D
Calculus
Derivative recovered from the limit (first principles)
Ex 4
E
Calculus
Definite integral giving an exact rational vs a decimal
Ex 5
F
Calculus
A limit that is not obvious (0/0 form)
Ex 6
G
ODE
Second-order, complex characteristic roots (oscillation)
Ex 7
H
ODE
First-order with initial condition (real-world growth word problem)
Ex 8
I
Traps
Exam twist: == vs true equality, and reading solve's output
Ex 9
Every symbol we use — solve, diff, limit, integrate, dsolve, discriminant, I — is unpacked at the moment it appears.
x 2 − 7 x + 12 = 0 .
Forecast: before reading on, guess: two nice whole numbers multiply to 12 and add to 7 . What are they?
Step 1 — Set up the symbol and expression.
x = symbols( 'x' )
solve(x ** 2 - 7 * x + 12 , x) # -> [3, 4]
Why this step? symbols('x') makes x an unknown that keeps its algebra (not a number). solve(expr, x) reads expr as expr = 0 — because any equation a = b becomes a − b = 0 , so "roots of one expression" is the universal form.
Step 2 — Understand the answer. SymPy factors x 2 − 7 x + 12 = ( x − 3 ) ( x − 4 ) internally. A product is zero only if a factor is zero, so x = 3 or x = 4 .
Verify: plug both back. 3 2 − 7 ⋅ 3 + 12 = 9 − 21 + 12 = 0 . ✓ And 4 2 − 7 ⋅ 4 + 12 = 16 − 28 + 12 = 0 . ✓
Here the twist is the discriminant . For a x 2 + b x + c , the discriminant is b 2 − 4 a c — the number under the square root in the quadratic formula. It answers one question: "are the roots real?" If it is negative, the square root of a negative number appears, and SymPy writes it with the symbol I , meaning − 1 (the "imaginary unit").
Definition The imaginary unit
I
I is SymPy's name for − 1 , i.e. the number whose square is − 1 . It appears whenever we take the square root of a negative number. On the board it lets us write answers like 2 ± 3 I that have no place on the ordinary number line.
x 2 − 4 x + 13 = 0 .
Forecast: discriminant = ( − 4 ) 2 − 4 ( 1 ) ( 13 ) = 16 − 52 = − 36 < 0 . Since it is negative, predict: no real answer , roots come as a complex pair.
Step 1 — Check the discriminant first.
a, b, c = 1 , - 4 , 13
b ** 2 - 4 * a * c # -> -36
Why this step? The sign of the discriminant tells us the kind of answer before we compute it — that's how you know which cell you are in.
Step 2 — Solve.
x = symbols( 'x' )
solve(x ** 2 - 4 * x + 13 , x) # -> [2 - 3*I, 2 + 3*I]
Why this step? − 36 = 6 I , and the quadratic formula gives x = 2 4 ± 6 I = 2 ± 3 I .
Verify: substitute x = 2 + 3 I :
( 2 + 3 I ) 2 − 4 ( 2 + 3 I ) + 13 . Since ( 2 + 3 I ) 2 = 4 + 12 I + 9 I 2 = 4 + 12 I − 9 = − 5 + 12 I , we get ( − 5 + 12 I ) − ( 8 + 12 I ) + 13 = 0 . ✓
The twist: sometimes an equation is not really a quadratic , or has no or infinitely many solutions. You must recognise the empty list [] and the "always true" case.
x + 1 = x + 2 and (b) 0 ⋅ x = 0 .
Forecast: (a) says 1 = 2 — impossible. (b) is true for every x . Predict: (a) → no solutions, (b) → SymPy cannot list "all numbers".
Step 1 — The impossible one.
x = symbols( 'x' )
solve(Eq(x + 1 , x + 2 ), x) # -> []
Why this step? Eq(lhs, rhs) builds a genuine equation. Subtracting: x + 1 − ( x + 2 ) = − 1 , which is never 0 . SymPy returns the empty list [] — the honest answer "no x works."
Step 2 — The always-true one.
solve(Eq( 0 * x, 0 ), x) # -> [] (SymPy also warns; the relation is trivially true)
Why this step? 0 = 0 holds for all x , so there is no specific root to return. Reading [] correctly (empty ≠ error) is the lesson.
[!mistake] Steel-man: "solve returned [], my code is broken."
It feels like failure. But [] is a valid mathematical answer : "no solution." Fix: treat an empty list as information, not a crash.
Verify: x + 1 = x + 2 ⇒ 1 = 2 is false for all x . ✓ (No root exists, matching [].)
The tool here is limit. A derivative f ′ ( x ) measures the instantaneous steepness of f ; it is defined as a limit:
f ′ ( x ) = lim h → 0 h f ( x + h ) − f ( x )
The fraction is the slope of the line through two nearby points; we squeeze the gap h to zero to get the slope at the point. See Limits and the definition of the derivative .
Worked example Recover the derivative of
f ( x ) = x 3 from scratch, then confirm with diff.
Forecast: the power rule says d x d x 3 = 3 x 2 . We will not assume it — we will derive it.
Step 1 — Build the difference quotient.
x, h = symbols( 'x h' )
f = x ** 3
q = (f.subs(x, x + h) - f) / h # ((x+h)**3 - x**3)/h
Why this step? .subs(x, x+h) slides the input forward by h ; the whole fraction is the slope of the red secant line in the figure.
Step 2 — Take the limit.
limit(q, h, 0 ) # -> 3*x**2
Why this step? limit(q, h, 0) asks "what does the slope approach as the two points merge?" — the geometric meaning of the tangent (blue line).
Step 3 — Cross-check with the built-in rule.
diff(f, x) # -> 3*x**2 (same!)
Why this step? diff uses the power rule, which is itself derived from the same limit — so agreement proves consistency.
Verify: expand ( x + h ) 3 = x 3 + 3 x 2 h + 3 x h 2 + h 3 . Subtract x 3 , divide by h : 3 x 2 + 3 x h + h 2 . Let h → 0 : 3 x 2 . ✓
An integral ∫ a b f d x is the signed area under the curve from a to b . The twist: SymPy keeps it exact (a fraction), and you choose when to collapse to a decimal with .evalf().
∫ 0 2 ( x 2 + 1 ) d x exactly, then as a decimal.
Forecast: antiderivative is 3 x 3 + x . Evaluate 0 → 2 : 3 8 + 2 = 3 14 ≈ 4.6 6 .
Step 1 — Definite integral (exact).
x = symbols( 'x' )
integrate(x ** 2 + 1 , (x, 0 , 2 )) # -> 14/3
Why this step? The tuple (x, 0, 2) triggers the Fundamental Theorem: ∫ a b f = F ( b ) − F ( a ) . SymPy returns the exact rational 14/3 , never a rounded 4.6666....
Step 2 — Only now, a decimal.
(Rational( 14 , 3 )).evalf() # -> 4.66666666666667
Why this step? .evalf() is the "please round now" button. We ask for it last , so no rounding error creeps into the algebra.
Verify: F ( x ) = 3 x 3 + x . F ( 2 ) = 3 8 + 2 = 3 14 ; F ( 0 ) = 0 . Difference = 3 14 . ✓
The twist: plugging in the target value gives 0 0 , which is undefined by direct substitution . This is exactly why the tool limit exists — it finds the value the expression approaches , even where it cannot be evaluated directly. See Taylor and Maclaurin Series for the deeper "why."
x → 0 lim x sin x .
Forecast: at x = 0 the fraction is 0 s i n 0 = 0 0 — indeterminate. But the graph shows the curve heading toward a clean value. Guess it.
Step 1 — Try direct substitution (and see it fail). Putting x = 0 gives 0/0 : no answer. That is why we need a limit, not a substitution.
Step 2 — Ask SymPy for the limit.
x = symbols( 'x' )
limit(sin(x) / x, x, 0 ) # -> 1
Why this step? limit inspects the behaviour near x = 0 , not at it, dodging the 0/0 trap.
Step 3 — See why via the series. sin x = x − 6 x 3 + ⋯ , so x s i n x = 1 − 6 x 2 + ⋯ → 1 .
(sin(x) / x).series(x, 0 , 4 ) # -> 1 - x**2/6 + O(x**4)
Why this step? The leading term 1 is the limit — the series makes the answer visible.
Verify: the constant term of the series of sin ( x ) / x at 0 equals 1 . ✓
An ODE (ordinary differential equation) links a function to its derivatives; solving it means finding the function , not a number. dsolve is the tool. The twist here: the characteristic roots are complex, which encode oscillation (cos , sin ). See Ordinary Differential Equations — characteristic equation method .
y ′′ + 9 y = 0 .
Forecast: guess y = e r x . Then r 2 + 9 = 0 ⇒ r = ± 3 I . Imaginary roots ± 3 I mean oscillation with frequency 3 : predict cos ( 3 x ) and sin ( 3 x ) .
Step 1 — Declare an unknown function .
x = symbols( 'x' )
y = Function( 'y' )
Why this step? Function('y') makes y a function of x (so y(x).diff(x, 2) means y ′′ ), unlike symbols which makes a plain unknown number.
Step 2 — Solve.
dsolve(Eq(y(x).diff(x, 2 ) + 9 * y(x), 0 ), y(x))
# -> Eq(y(x), C1*sin(3*x) + C2*cos(3*x))
Why this step? The characteristic equation r 2 + 9 = 0 has roots ± 3 I ; complex roots α ± β I give solutions e α x ( cos β x , sin β x ) . Here α = 0 , β = 3 → pure waves, drawn in the figure.
Step 3 — Read C 1 , C 2 . These are arbitrary constants — SymPy returns a whole family of solutions because no starting condition was given.
Verify: let y = sin ( 3 x ) . Then y ′′ = − 9 sin ( 3 x ) , so y ′′ + 9 y = − 9 sin ( 3 x ) + 9 sin ( 3 x ) = 0 . ✓ (Same for cos ( 3 x ) .)
Bacteria. A colony grows so that its rate of increase equals half its current size: y ′ = 2 1 y . At time 0 there are 20 bacteria. Find y ( x ) and the population at x = 4 .
Forecast: "rate proportional to size" is exponential growth. General shape C 1 e x /2 ; the start y ( 0 ) = 20 pins C 1 = 20 . Predict y ( x ) = 20 e x /2 .
Step 1 — Translate words to an ODE. "rate of increase" is y ′ ; "equals half its size" is 2 1 y . So y ′ − 2 1 y = 0 .
Why this step? Every growth word problem becomes an equation linking y ′ to y — recognising this is the whole skill.
Step 2 — Solve with the initial condition.
x = symbols( 'x' )
y = Function( 'y' )
dsolve(Eq(y(x).diff(x) - Rational( 1 , 2 ) * y(x), 0 ), y(x),
ics = {y( 0 ): 20 }) # -> Eq(y(x), 20*exp(x/2))
Why this step? ics={y(0): 20} supplies the starting count, which fixes the single arbitrary constant of a first-order ODE.
Step 3 — Population at x = 4 .
( 20 * exp(Rational( 1 , 2 ) * 4 )).evalf() # -> 147.781...
Why this step? Substitute x = 4 : 20 e 2 ≈ 147.78 . Units: bacteria (a count).
Verify: y ( 0 ) = 20 e 0 = 20 ✓ (matches the start). Check the ODE: y ′ = 20 ⋅ 2 1 e x /2 = 2 1 ( 20 e x /2 ) = 2 1 y . ✓
Worked example Two classic exam traps in one problem.
(a) Does (x+2)**2 == x**2 + 4*x + 4 return True?
(b) From solve(x**2 - 2, x), extract the positive root as a decimal.
Forecast: (a) they are mathematically equal — but predict == says False. (b) roots are ± 2 ; the positive decimal is 1.4142 …
Step 1 — The == trap.
x = symbols( 'x' )
(x + 2 ) ** 2 == x ** 2 + 4 * x + 4 # -> False
simplify((x + 2 ) ** 2 - (x ** 2 + 4 * x + 4 )) == 0 # -> True
Why this step? == checks structural identity (same tree), not mathematical equality. The two forms are written differently, so == says False. To test true equality, simplify the difference to 0 .
Step 2 — Reading solve's list.
sol = solve(x ** 2 - 2 , x) # -> [-sqrt(2), sqrt(2)]
sol[ 1 ].evalf() # -> 1.4142135623731
Why this step? solve returns a list of symbolic roots (with sqrt), not a single number. We index sol[1] for the positive root, then .evalf() to get a float.
Verify: ( 2 ) 2 − 2 = 2 − 2 = 0 ✓, and ( x + 2 ) 2 − ( x 2 + 4 x + 4 ) = 0 identically ✓.
Recall If a quadratic's discriminant is negative, what does
solve return and why?
A complex-conjugate pair like 2 ± 3*I, because negative produces the imaginary unit I = − 1 .
Recall
solve(...) gave []. Is that an error?
No — [] means "no solution exists," a valid mathematical answer (e.g. x + 1 = x + 2 ).
Recall Why compute
lim x → 0 sin ( x ) / x instead of substituting x = 0 ?
Direct substitution gives the indeterminate 0/0 ; limit finds the value the expression approaches (= 1 ).
Recall Complex characteristic roots
± 3 I turn into what kind of ODE solution?
Oscillations: C 1 sin ( 3 x ) + C 2 cos ( 3 x ) (frequency 3 ).
Recall Why is
20*exp(x/2) the answer to y ′ = 2 1 y , y ( 0 ) = 20 ?
"Rate ∝ size" → exponential; the 2 1 sets the exponent, and y ( 0 ) = 20 fixes the constant.
"Check the sign, read the list, limit the trap." — discriminant sign tells you real vs complex; solve gives a list ; a 0/0 needs limit, not .subs.
Limits and the definition of the derivative (Ex 4, Ex 6)
Taylor and Maclaurin Series (why sin x / x → 1 )
Ordinary Differential Equations — characteristic equation method (Ex 7, Ex 8)
Lambdify — bridging SymPy to NumPy for plotting (turn these symbolic answers into curves)
NumPy — numerical arrays (floats vs the exact rationals above)
What does a negative discriminant produce in solve? A complex-conjugate root pair using I (√−1).
What does solve return when no solution exists? An empty list [].
How do you get the derivative of x³ from first principles? limit(((x+h)**3 - x**3)/h, h, 0) → 3*x**2.
Value of integrate(x**2+1,(x,0,2))? 14/3 (exact rational).
Value of limit(sin(x)/x, x, 0)? 1.
Solution of y''+9y=0? C1*sin(3*x)+C2*cos(3*x).
Solution of y'=y/2 with y(0)=20? 20*exp(x/2).
Why does (x+2)**2 == x**2+4*x+4 give False? == tests structural identity, not math equality; use simplify(a-b)==0.