User:Addemf/sandbox/Technical Reasoning/Python Programming

We will often use Python, especially the SymPy library, throughout this course for two different purposes.

(Python ⇒ Math) One of them is to use Python like a calculator. This will take long and tedious calculations that a human should never have to do, and let the computer do them for us.

(Math ⇒ Python) Another is to use math to help us understand Python.

In what follows, I will present effectively a "lightening speed" introduction to basic programming in Python. The goal here is not to be comprehensive, but to communicate the essential ideas just enough so that we will be able to use SymPy and return to the overall mission of this course.

Getting Started
You will need to be able to run Python programs, and for that you need Python installed.

This is often a difficult topic, because computers change, and Python changes, all the time. Therefore the instructions that I could write now, may quickly become outdated.

My recommended solution is to search for instructions, on the web or on YouTube, for how to install Python and then a IPython notebook. If you want a very specific recommendation, you could install Anaconda, which will install both Python and Jupyter notebooks easily.



My personal setup is an installation of IPytnon notebooks inside VSCode. I have a screenshot to the right showing this setup. One cell of the notebook contains

Then I ran this cell by clicking the "play" symbol, ▷, to the left of the cell. (There are also keyboard commands to run a cell, which may differ for different computer systems.) The result was

2

printed below the code cell.

Hello World
Whenever learning a new programming language, you should always start by writing a "Hello World!" application. This is what the programming world calls any program which simply prints "Hello World!" to the console.

A "Hello World!" application is basically a demonstration to yourself that you are at least able to get a very simple application to run a very simple process.

Therefore you should be able to write the following code into a cell.

If you run the cell, it should print the text "Hello World!" below it.

You would get the same result if you removed the  command, with its parentheses (i.e. if you only had the code  ).

This is because a cell always prints the last value in the cell.

Therefore if you ran the following cell,

it would only print the last value, which is.

On the other hand, if you ran the cell,

the explicit command to print each value, would cause all three values to be printed below the code cell.

Int, String, Float, Bool
Look at the following script, which prints out four different lines of text, one for each  command.

Before you run it, guess what each one will print. Then run it and see if you were right.

1+1
Probably you guessed correctly about what  does, which is that it computes the number   as   and then prints   to the console.

1/2
Next, you probably guessed that  would print some kind of representation of one-half. And you were right, although it is worth noticing that the result is  which is a special kind of number.

Numbers which have a decimal in them are called "floats". Numbers which do not have decimals in them, like, are often called "integers" or "ints" for short.

We will later see that floats and ints behave in significantly different ways.

1<2
Notice that  is an operation on numbers, but its value is. It returns the value  because it is true that 1 is less than 2.

This  value is called a "boolean value". Boolean values are called "bools" for short.

Of course there is also the boolean value  and you are encouraged to write a script which prints this as well.

True
Next we called, just to demonstrate that one could just directly print boolean values. They don't have to come from other operations which generate boolean values.

1<2 and 2
2 ===

You are already very familiar with the fact that you can perform operations on numbers, like how  is an operation on numbers which evaluates to.

Well, boolean values are similar and they have an  operation. In this example,  evaluates to. also evaluates to, because the   operation will say whether or not the two values are equal.

So the code  evaluates to   which in turn evaluates to , which you see printed to your console.

True and False
The expression  evaluates to , because the   operation will only evaluate to true when the left side AND the right side are both simultaneously.

You are encouraged to guess and then check what the following prints.

Check that the way to "exponentiate" (i.e. compute exponents, such as $$2^3=8$$) in Python is to use the  operation.

To do so, run the following.

Explain why  evaluates to a negative number. Why isn't it equivalent to ?

Strings
A string is a way of representing text inside a Python program. We saw a string already in the Hello World! program, which contained the string.

Strings are created by double-quote-marks. Inside of them you can place text with most ASCII characters.

There are a few exceptions to what you're allowed to put in a string. For one thing, tabs and line-breaks cannot be placed in a string directly. For instance, the following code

will only cause an error if you try to run it.

It is possible to put line breaks into Python strings but this won't be important for us, so we won't discuss it.

Now as an exercise, read the following code and predict what you think it will do. Then run it to see if you were right.

When text is in a string, the computer treats it merely as text and does not try to interpret it. Therefore the code above only prints, because the computer does not consider text to be something it's supposed to evaluate.

It is common to need to break strings into their parts. To do so we may "index" the string in order to access each character. If you run the following code, you will see the string "Hello" indexed at each of its available indices.

The code  will return the first letter of the string, which is. It may seem strange to use the number  for the "first" letter, but this kind of convention is common across all programming languages.

The convention that indices start at 0 is called "zero-indexing". There is a good reason for using zero-indexing, which has to do with how information is stored in a computer. However, we don't need to discuss it, and the interested reader is free to search for explanations.

Now if you run the code above, you will probably notice that it gives you an error message. This is natural because we called on six different indices, from  to. However the string  has only five letters! Therefore that last index has gone "out of bounds".

Errors don't usually hurt anything on your computer, but they do cause your program to completely halt its execution, and therefore you want to avoid them.

For example, the following code does not print anything except an error. In particular, the first line causes a "divide by zero" error, and therefore the line after it never runs.

If a string has n characters in it, then what is the largest possible index of the string?

Not only can you extract one character at a time from a string, but you can also extract entire substrings by specifying a start- and end-index. This is sometimes called "string slicing".

The code above prints. Notice that the blank space is included because it truly is a character in the string!

Notice also that  includes the characters at indices 5, 6, 7, 9, and 10. It does not include the exclamation mark at index 11!

This may seem odd because the number 11 was included in the range of indices for the slice. However, a convention that many programming languages use, is to include the left-most index of a slice and exclude the right-most index.

This convention often has no name, but I have seen some people refer to it as the "clopen convention". The word "clopen" is a portmanteau of the words "closed" and "open".

The word "closed" often indicates that, for a range of values, we include the end-points. Therefore the closed interval from 2 to 17 would include both 2 and 17.

The word "open" often indicates that the range does not include the end-points. So the open interval from 2 to 17 does not include 2 and does not include 17.

Therefore if an interval is "clopen" then we include the value on the left, and exclude the value on the right. (I suppose if an interval excluded the value on the left and included it on the right we might call such an interval "opesed".)

At this point we know how to The last thing that we'll see is how to join two strings together. Run the following code and you'll see how it's done!
 * Write a string.
 * Break a given string into parts.

Lists
Here is an example of a list:

A list is like a bundle of various things. We usually bundle things together because they are somehow related to each other.

For example, we might put the heights of people together into a list, so that we can compute their mean. This is done in the following code.

A list is a lot like a string, in that one can perform list indexing, slicing, and joining.

You can almost think that strings are just lists where all the entries are required to be text characters. The two types of objects certainly support the same list indexing syntax.

However, that is not quite true. After we discuss variables a bit, it will be possible to see one important difference between the two.

Variables
Variables allow you to store information so that you can recall that information later. To store the value  into the variable , one writes



You can choose most names which are just lower-case letters, it doesn't have to be. For instance  stores the value   into the variable named.

But there are constraints.

For one thing, the words  and   and   and several others, are "reserved words" which mean something in Python. Therefore your variable name is not allowed to be these or the other reserved words. Do an internet search for "Python reserved words" to find all the others.

For another thing, there are some rules about variable names. Do an internet search for "valid Python variable names" to see the rules. But if you stick to names that are only lower-case letters, then as long as you avoid the reserved words, you'll be safe.

The practical value of using variable is hard to demonstrate until we discuss loops and functions — but the value of loops and functions is hard to demonstrate without first seeing variables. Pedagogically, it's a bit of a chicken-and-egg scenario.

So at least for now, trust me that it is useful to have variables, and you will see why shortly.

Predict what the following code prints.

Also predict what the following code prints.

List Mutation
List mutation is the act of taking a given list and changing its contents after it has been created.

Index assignment the first kind of mutation that we will look at. It is the act of taking a list and changing an index-specified entry, demonstrated below.

As usual, I recommend reading it and trying to guess what it will print before running it.

The list is initially declared as  and stored into the variable.

On the next line we perform the index assignment, by accessing index  and assigning it the value.

Finally we print the updated list, which results in showing the list.

Why is it not possible to demonstrate index assignment without the use of variables?

There are other ways to mutate a list, though. One of them is to "pop" the last element off of the list, demonstrated below.

This code first declares the list and assigns it to the variable. It then pops the last element off of the list, which means that it removes the. Then it prints the result,.

Not only does  remove the last element, but it also "returns" that element. Hence the line  puts that value, , into variable. Hence that is printed by.

The Danger of List Mutation ☠️
Recall what we said earlier about copying the value of one variable into another variable, demonstrated in the following code.

This printed  because there was no link between the variables   and   after the initial assignment of  's value to.

But now consider the following example. Again, predict what it will do and then run it.

Perhaps surprisingly, this time it prints ! What explains this behavior?

The difference is the fact that lists are mutable, unlike ints. An int cannot change, it just is what it is.

The reassignment  in the first example does not change the integer 1. Rather it simply changes the value that  refers to.

On the other hand, the code  does not change what   refers to. continues to refer to the same list, but that list changes an entry, because of the index assignment. Therefore  and   continue to refer to the same list, and that list has now changed. Therefore when you call on, you still see that list, with its entries mutated.

This is a danger that you want to be aware of!! I use two exclamation marks advisedly, because it risks making this text seem unprofessional — but it is worth it, in order to emphasis how important this warning is.

When objects mutate, it is easy for the programmer to forget or lose track of exactly how objects are mutating throughout the run of the program. You can end up spending many hours trying to trace the cause of a bug in your program, for it to end up being an issue like this. I speak from experience.

Control Flow
Suppose that we want to find the largest element in a list of numbers. Here's how you can do that.

The code below makes an example, arbitrary list

Then, roughly speaking, by saving the "first" element into a variable,. Then for each element in the list, check whether it is bigger than the content of. If so, update  with the new bigger value. Otherwise, just move on to the next element.

You may not initially understand everything that is going on in this code, but hopefully it gives you a rough idea of why we want to have "for-loops" and "if-then statements".

For-loops
Let's start by understanding for-loops better. The following code, and the result that it prints, really shows you almost everything you need to know about general for-loops.

Every for-loop has the form

The "Head"
The first line of a for-loop is called the "head" of the for-loop. It always begins with.

It must then be followed by some variable, you are free to choose the variable name. Earlier I used the variable name  and later I chose , but in general one is free to pick any name not already in use earlier in the program.

After that we have a thing which I have called above "iterable". In the two examples above, the "iterable" object was a list.

The earlier example this list was first stored in the variable  and then used in the for-loop declaration. In the later example, the list was written directly into the for-loop declaration. Either works, so again, you're free to accomplish this however you prefer.

But there are other iterable objects as well. One example is a string. Try predicting the output of the following code and then run it.

Finally the head of the for-loop must end with a colon,.

The "Body"
The body must follow the head by a line-break and then by an indent. The standard indentation is four spaces, although many modern interpreters may understand other conventions.

The body is any code at all, but it must always be maintained at the same level of indentation. Any code which is un-indented back down to the level of the for-loop head will no longer execute as part of the for-loop.

To demonstrate this, see the following two examples. As usual, predict the output of each and then run them.

The former has behavior that probably makes sense when you see it.

The latter causes an indentation error. That is to be expected because the un-indented  has "reset" the indentation level back down to the the baseline. Therefore after this line, the occurrence of an indent is unexpected.

If you un-indent the line  then the error message will go away but it will print something very different from the first code example.

This demonstrates that, in Python, white space is meaningful. In cases like this, the occurrence or lack of white space will change the behavior of the program. In many other languages, like C and Java, that is not true.

If-Statements
Here is a demonstration of the use of if-statements.

Every if-statement has the structure

The "Head"
The first line of an if-statement is called the "head" and always begins with.

It must then be followed by some kind of expression which evaluates to a boolean value, called the "condition". In the example directly above there are three different if-statements, which have the conditions  and   and.

A condition must evaluate to a boolean value. These three examples of conditions evaluate to the values,  , and  , respectively.

As with for-loops, the head of an if-statement must end with a colon.

The "Body"
The body of an if-statement will run when the condition evaluates to  and otherwise the body is skipped over.

Just as with for-loops, the indentation is meaningful. The body only extends to lines which are indented one level beyond the declaration of the head.

As you can see in the example directly above, control flow can be "nested". In this example, we have nested two if-statements inside of an if-statement.

("Control flow" is the name of this class of structures, which includes for-loops, if-statements, and more.)

Every nested control flow must maintain its body at a level of indentation which is one more than the indentation of the head.

Predict the result of the following code.

The Max Example
Let us now return the example at the start of this section, which finds the maximum of a list.

Hopefully the logic is now clear. The list is declared, the initial largest value is set to the first element of the list.

But then, the variable  "absorbs" each next value in the list. Initially  absorbs the value   and then we run the body

In this context,, which is some notation to express the currently held values of each of the variables.

Therefore  evaluates to   in this context, which in turn evaluates to. Therefore the if-statement body never runs.

But now that this is done, the for-loop continues by making  absorb. Therefore in this context  and we again run the body of the for-loop.

This means evaluating

where the condition now evaluates to  and therefore the body of the if-statement does execute this time. Therefore we run  which results in.

We repeat this again and again, with  next absorbing   and then   and so on.

As this goes, eventually  will get updated to become. After that,  will never absorb a larger value and therefore   will never be reassigned after this.

Therefore when the run of the for-loop has finished,  and therefore this is the value printed and the end.

Functions
Here is an example of a function. This function takes as an argument some number, and "returns" that number plus one.

To see this function "in action" you can run a cell containing

Here is an example of a function which takes as an argument some text and returns a list of all of the letters in the text. (It uses the  method, which puts a single element onto the end of a list.)

You can see its result in the following code

Function Definition
A function definition has the form

This is specifically the form of a function definition with only one parameter. It is possible for a function to have zero, one, two, or any other natural number of arguments. To begin with, we focus on the case of one argument.

The function name is a lot like a variable name, in that the choice is up to you. But just know that this is the name of the entire function, and this is what you will use to call the function later.

The parameter is a variable name, but it is a variable that is only used inside the function. To see the importance of this, consider the following example.

The code above prints  first and then. Notice that although the two names (both ) are the same, they do not "conflict". The variable  outside the function, is not affected by the parameter   inside the function.

Note that you must first state the function name and then write parentheses with the parameters inside. After the close parenthesis is a colon which ends the function definition head.

As usual, the body occurs on the next line after an indent. The body may be any valid code that you would like to have run, every time the function is called.

Function Call
The point of writing a function is to call it somewhere else. Usually it is important to call it repeatedly.

A function call (with one argument) has the form

For instance, referring back to the previous  example, calling   evaluates to

with the context that. Therefore this evaluates to  which in turn evaluates to. Hence this is the value.

Returning Versus Side-effecting
An important distinction with functions, is whether a function returns anything versus whether it has side-effects.

Consider the two functions, and their calls, demonstrated below.

Let's discuss the four print-outs in turn. First, the call to  receives the list. Per the body of the function, it constructs the new list  and returns it. This return value is what is then printed.

However, the original list  was never affected by the function. Rather, a new list was constructed by the code. Because this was returned, this is what the function call  evaluates to, and hence why it was printed.

But because the original list was unaffected, then the second call to print,, prints the original list.

On the other hand,  does not return anything!  Rather, this function takes the input list and mutates it by appending   onto the end of the original list.

Because this function does not return anything, then therefore  evaluate to. This is, so-to-speak, "the return value when there is no return value". You can get philosophical about this if you like, but I'll just move right along.

But although  returns nothing, it still has a "side-effect", which is namely to append zero onto the input list. This is why the final call to  now prints the original list but with   appended to its end.

Functions Attached to Instances
Certain kinds of objects come "prepackaged" with functions attached to them.

We have already seen the use of the  function, which is a function that comes "prepackaged" with lists.

For another example, lists have an attached  function, which you see demonstrated in the following code.

For one more example, strings have an attached  method, demonstrated below.

When an object has an attached function which requires one argument, a call to that function takes the form

Note that the example of  required no arguments. Although one calls the function by not writing any arguments between the parentheses, it is still necessary to write the parentheses.

SymPy


The SymPy module allows us to do "symbolic" mathematical manipulations.

You first have to install the module before you can import it in your script.

The way to install it can depend on how you installed Python, so you'll need to consult whatever documentation is right for your setup.

In the image to the right, I show my setup in VSCode using IPython Notebooks. Other setups are possible, and you may be interested to either use Jupyter or to develop in a more dedicated IDE. Again, I leave all of the setup details to the reader, as these things can differ so much across time, different machines, and personal taste.

In the notebook, one cell contains the code

The result below it is

IPython console for SymPy 1.12 (Python 3.11.4-64-bit) (ground types: python) These commands were executed: >>> from sympy import * >>> x, y, z, t = symbols('x y z t') >>> k, m, n = symbols('k m n', integer=True) >>> f, g, h = symbols('f g h', cls=Function) >>> init_printing Documentation can be found at https://docs.sympy.org/1.12/

The short story is that this set up some nice things "behind the scene" which we don't need to worry about too much right now.

But because of that initialization, if we now run

then it will print an acceptable Unicode "√x" as a representation of "square-root x".

But below that is a cell which contains

which renders the much more attractive LaTeX rendering,

$$\sqrt x$$

This just demonstrates the superiority of  over   in this setting.

Import Before Using
Note that the import and initialization commands must always run, before you are able to use anything from SymPy. That is to say, you must run a cell containing the following code before you try to run any code which uses SymPy.

Throughout the rest of this page, every code cell that I will display, assumes that the code above has already run at least once in order to work.

Numeric Values in Python
Most computer processes use numerical methods to represent mathematics. In essence, numerical methods do not maintain the "structure" of values, and instead prefers decimal approximations. We've already seen this with the fact that  evaluates to. This seems fine because the two numbers seem exactly the same, just written in different formats.

However, some numbers will have decimal sequences which are infinite. Even the very simple number  gets represented in numeric Python as

But because a computer is necessarily finite, while this decimal expansion is infinite, then the computer must cut this sequence off at some point. This represents a mere approximation of the number 1/3 rather than a truly equal representation.

Notice that if Python had a truly faithful representation of fractions and decimal values, the following code would return.

However, because of rounding errors, this code evaluates to.

This can sometimes cause a variety of problems, from issues of accuracy, to issues of slow computers, to other kinds of issues that result from the loss of the "structure" of the number 1/3.

What if we wanted to know, for instance, what the denominator is? If the computer can only show you its decimal, then it cannot easily tell you the denominator.

Symbolic Values in SymPy
We can contrast the numeric evaluation of basic Python with the symbolic evaluation in the SymPy module. The following shows how SymPy represents fractions (or "rational numbers" as they're called by mathematicians).

When this cell is run, it prints

$$\frac 1 2$$

which maintains the "structure" of the number as a ratio of two integers.

Because it maintains this structure, it does not run into the same problem of rounding that the numeric method above did.

prints the correct answer,.

This is because it does not convert everything to decimal approximations and then adds the approximations to deliver even more approximate results.

Instead, it uses the normal rules of fraction addition. Which is to say that it puts all of the fractions into a common denominator, adds the resulting numerators, and then simplifies the fraction.

Moreover, the symbolic nature of SymPy is what allows it to represent mathematical variables. For instance, if you run

it will print the variable x, which it would not do without SymPy. This is because, when we called  in the beginning, this caused SymPy to recognize x and y and a few other standard letters as mathematical variables.

Because it recognizes these objects as variables, they can therefore feature in mathematical expressions, like $$\sqrt x$$, which we also saw earlier. This not only allows for elegant printing, using the  command. It also allows us to manipulate mathematical expressions, as if they were mathematical expressions.

Take for example the following code. It constructs two different mathematical expressions and then multiplies them together and displays the result.

Moreover, because SymPy understands them as mathematical expressions, I can even ask it to simplify the result using the mathematical rules of distribution. This is done with the call to.

A Caveat
Symbolic manipulation is not superior to numeric manipulation in every way. Symbolic manipulations can sometimes take longer because of the "computer overhead" of maintaining the structure of objects.

Also, if the structure is ever needed for numeric computations, it is always possible to "build it from scratch" rather than using a module which does all of the structuring for you.

So the main virtue that symbolic manipulations offer is ease with which the structure can be preserved. It has all been done for you, in advance, so that you merely have to use what is there rather than building it yourself.

SymPy as Calculator
Up to this point I have been showing the use of SymPy with so-called "qualified names" of functions. For instance, to produce the square-root of x I've chosen to write.

This is not strictly necessary, because again, calling  has automatically set a few things up behind the scenes. We already saw that it made x into a mathematical variable. It has also imported some of the most commonly useful mathematical functions, like the square-root.

Therefore we could simply call  and Python would already know to look in the SymPy suite of functions to find this one.

You're encouraged to predict the output of the following code and then run it.

This again demonstrates the essentially symbolic nature of SymPy. When it reads  it simply tries to express this as a mathematical object, without trying to approximate it with a decimal value.

Of course you might wonder "Well what's the point of that? I already know it's the square-root of 2 because I entered it that way.  Of course I want to know the decimal approximation."

Well, for one thing, that is not always true for everybody. Consider the possibility that you have the expression $$\sqrt{6}\cdot \sqrt 2$$ and would like to know the simplest expression of this value.

This returns the simplified, but still exactly equal value $$2\sqrt 3$$.

But of course sometimes one truly does want a decimal approximation. This, too, is possible.

Find a simplified expression for the number $$\frac{1}{1+\frac{1}{1+\frac 1 2}}$$. Then find its decimal value.

Hint: You may want to declare a variable with  and then use this to form the rest of the expression.

You would not get a symbolic representation if you entered.

SymPy As Solver
A somewhat more impressive feature of SymPy is its ability to solve equations!

Consider the equation $$x^2+2x+1 = 0$$. This is solved by the following script.

This prints the list of all solutions. In this particular case there is only one solution, $$-1$$, and therefore it prints the list.

When you simply hand an expression, like, to the solver  , it will make a few assumptions. One assumption is that, if the expression has just one variable, then it will solve for that variable (duh, what else would you possibly want, right?).

Another assumption is that it assumes that the "other" side of the equation is set to 0.

If any equation is not set to zero then of course there is always an equivalent equation which is. For example


 * $$x^2 + 2x + 1 = x - 2$$

is equivalent to


 * $$x^2 + x + 3 = 0$$

which you can find by merely subtracting the right-hand side to the left-hand side.

Therefore if you wanted solutions to $$x^2+2x+1=x-2$$ they are given by

which prints $$\left[ - \frac{1}{2} - \frac{\sqrt{11} i}{2}, \ - \frac{1}{2} + \frac{\sqrt{11} i}{2}\right]$$.

This is the list of all solutions ... in the complex numbers! Pretty freaking impressive, yeah?

The following confirms that in fact $$-\frac 1 2 - \frac{\sqrt{11}i}2$$ is a solution to the original equation.

The above should display the same value twice, hence the two sides are equal, hence  is a solution to the equation.

However, you probably would hope that the following would be another way to confirm the solution.

And yet this prints. Why?

SymPy checks for the equality of expressions rather than the equality of values.

You can see in the following example that the two expressions are "equivalent" and yet SymPy thinks they are unequal.

But if you force SymPy to expand first and then check for equality, it will then recognize the equality of the expressions.

Pick any way that you prefer, and show that $$-\frac 1 2 + \frac{\sqrt{11}i}2$$ (the other solution generated by the solver, earlier) is also a solution to the equation $$x^2+2x+1=x-2$$.

One of the really useful features, which shows the power of using Python programming while doing your math work, is the ability to re-use code.

For instance, suppose that you are asked to evaluate the function $$f(x) = \sqrt{x^2+1}$$ at the inputs $$x=-1,0,1,2,3,4$$.

Find decimal approximations for the function $$f(x)=\sqrt{x^2+1}$$ at the inputs $$x=-1,0,1,2,3,4$$.

SymPy Graphing
I don't anticipate needing to plot many functions in this course, but it is at least nice to know that SymPy can do everything that a graphing calculator can.

For this you will need the additional Python module, matplotlib. You should seek out instructions on how to install it, which will depend on how you installed Python.

Once you have it installed you should be able to run the following cell and see a plot of the function $$f(x)=x^2$$ on a reasonably readable graph.

The line  is necessary for notebooks to show the plot. If you are working from a pure console or other IDE, then you may not need this command.

SymPy and the Exotic
So far, SymPy is a particularly beautiful graphing calculator, which is much easier to use than those clunky hand-held brick TI things that I hate so much. Getting used to the commands for Python and SymPy takes no more work than a graphing calculator does. And once you get used to it, you'll be twice as fast at entering what you want it to give you.

In fact, when combined with Python programming, I have frequently been many times more than twice as fast.

But this is only the beginning of what makes SymPy cool.

If you have any familiarity with calculus, then you may be excited to know that SymPy can calculate derivatives and integrals for you. And not just numerically, but also symbolically! Ok, for sufficiently advanced graphing calculators, they can do that too.

But SymPy definitely supports branches of mathematics that no graphing calculator does. One of the first exotic branches of mathematics that we will encounter in this course is that of logic.

You may not yet fully understand what the following code produces. But after the end of the first section of this course, you will − and I think you'll appreciate all of the computation that SymPy is relieving you of the duty of performing.