This guide will cover the basic of "imperative" programming in Python - controlling the flow of your code using loops and conditionals
In this guide, we will learn two basic types of 'flow control' structures that will allow us to control how the code within our scripts is executed and allow us to start writing more complex algorithms.
Up to this point, our scripts have been pretty basic—limited to only executing in a top-down order with one command or operation per line. The following two concepts—conditionals and loops—are the two basic 'flow control' structures which can actually alter the sequence in which our code is executed, thereby creating more complex behavior and allowing us to implement more interesting functionality.
Conditionals are structures within a script which can execute different lines of code based on certain 'conditions' being met. In Python, the most basic type of conditional will test a Boolean to see if it is True
, and then execute some code if it passes:
b = True
if b:
print('b is True')
Here, since b
is in fact True
, it passes the test, causing the code that is inset after the if b:
line to execute. Try to run the code again, this time setting b
to False
to see that nothing happens. In this case, if b
does not pass the test, the entire block of inset code after the first conditional line is skipped over and ignored. In this code, if b:
is shorthand for if b == True:
. If you want to test for Falseness, you could use the Python shorthand if not b:
or write the full if b == False:
(if b != True:
should also work—in programming there are often multiple ways to do everything!).
In Python, a line ending with a colon (:
) followed by lines of code inset with tabs is a basic syntax for creating hierarchical structure, and is used with all code structures including conditionals, loops, functions, and classes. The trick is that Python is very particular about how these insets are specified. You have the option of using tabs or a series of spaces, but you cannot mix and match, and you have to be very explicit about the number of each that you use based on the level of the structure. For instance, this code:
b = False
if b:
print('b is True')
print('b is False')
will skip both print()
lines if b is False
. However, by deleting the indent on the last line, you take that line out of the nested structure, and it will now execute regardless of whether b
is True
or False
:
b = False
if b:
print('b is True')
print('b is False')
On the other hand, if you inset the last line one level further:
b = False
if b:
print('b is True')
print('b is False')
You will get an error saying
IndentationError: unexpected indent
which means that something is wrong with your indenting. In this case, you have indented to a level that does not exist in the code structure. Such errors are extremely common and can be quite annoying, since they may come either from improper indentation, mixing spaces with tabs, or both. On the bright side, this focus on proper indenting enforces a visual clarity in Python scripts that is often missing in other languages.
Coming back to conditionals, if a conditional test does not pass and the first block of code is passed over, it can be caught by an else
statement:
b = True
if b:
print('b is True')
else:
print('b is False')
In this case, when b
is True
the first statement will execute, and when b
is False
the second statement will execute. Try this code both ways to see.
In addition to using booleans, you can also create conditionals using various comparison operators. For example, a conditional can test the size of a number:
num = 7
if num > 5:
print('num is greater than 5')
Or the contents of a string:
t = 'this is text'
if t == 'this is text':
print('the text matches')
In this example I use the double equals (==
) operator to check if one thing equals another. This is the standard way to check equality, since the single equals (=
) is reserved for assigning values to variables. The most common comparison operators are:
==
or is
- check if two values are equal!=
or is not
- check if two values are not equal>
- check if one number is larger than another>=
- check if one number is larger or equal to another<
- check if one number is smaller than another<=
- check if one number is smaller or equal to anotherIn all cases, a succeeding condition will compute to a value of True
, while a failed condition will compute to a value of False
. You can check this by passing a conditional statement into the type()
function and observing that the type is bool
:
print(type(5 > 4))
Between the if ...:
and else:
statements, you can also use any number of elif ...:
(a concatenation of else and if) statements to chain together conditions to create more complex logics, for example:
num_1 = 3
num_2 = 7
if num_1 > 5:
print('num_1 is greater than 5')
elif num_2 > 5:
print('num_2 is greater than 5')
else:
print("they're both too small!")
This creates a chain of tests that happen in order. If the first test passes, that block of code is executed, and the rest of the conditional is skipped. If it fails, the second test (written after the elif
keyword) is analyzed, and so on. If none of the tests pass, the code following the else:
statement is executed.
In Python, you can also combine multiple tests within a single line of code by joining them with logic operators:
and
- returns True
if both conditions are True
, False
otherwiseor
- returns True
if either conditions are True
and False
only if they are both False
not
- added in front of any conditional or boolean, this operator reverses the boolean, making a True
value False
and vice versa.Using these logic operators we can write the following compound conditionals:
num_1 = 3
num_2 = 7
if num_1 < 5 and num_2 < 5:
print("they're both too small!")
if num_1 < 5 or num_2 < 5:
print("at least one of them is too small!")
if not num_2 < 5:
print("num_1 is at least 5")
Finally, we can create a conditional to check if an item or value is stored within a list using the spacial in
operator. We can also combine it with not
to check if something is currently not stored inside a list. For example:
myList = [1, 2, 3, 4, 5]
if 5 in myList:
print("5 is in the list")
myDictionary = {'a': 1, 'b': 2, 'c': 3}
if 'd' not in myDictionary.keys():
print("there is no item with a key of 'd'")
Loops are code structures that allow certain lines of code to repeat multiple times under specific conditions. The most basic type of loop is one that iterates over each value within a List:
fruits = ['apples', 'oranges', 'bananas']
for fruit in fruits:
print(fruit)
The for *item* in *list*:
structure is the most basic way to construct a loop in Python. It will run whatever code follows this line once for each item in the List, each time setting the current item to the variable specified before the in
. In this case, it will run the print(fruit)
code three times, once for each fruit in the List. Every time the code is run, the variable fruit
is set to a different fruit in the List in order. This type of loop is often used to apply a certain analysis or processing to every element within a List.
You can do the same basic kind of iteration on a Dictionary using the .keys()
method, which will return a List of all the Dictionary's keys, allowing you to iterate over each entry:
myDictionary = {'a': 1, 'b': 2, 'c': 3}
for key in myDictionary.keys():
print myDictionary[key]
If you run this code, you will see that the entries are not returned in the same order that they are typed. This is because Dictionaries, unlike Lists, do not enforce a specific order. However, iterating through the keys using the .key() function will ensure that you go through each item in the Dictionary once.
In addition to iterating through every item in a List or Dictionary, loops are often used to simply repeat a particular piece of code a specific number of times. For this, Python's built-in range()
function is very useful. range()
takes an integer as an input and returns a List of integers starting at 0, up to but not including that value:
print(range(5))
Using the range()
function, we can set up a loop to iterate a specified number of times like this:
for i in range(5):
print('Hello')
This will simply run the code inside the loop five times, since in effect we are creating a list of five sequential numbers, and then iterating over every item in that List. In addition, we are also storing each successive number in the variable i
, which we can also use within the loop. A common example is to combine both strategies by tying the range()
function to the length of a List (using the len()
function), and then using the iterating number to get items from that List one by one:
fruits = ['apples', 'oranges', 'bananas']
for i in range(len(fruits)):
print(fruits[i])
Although this might seem redundant given the first example, there are times when you want to build a loop that has access to both an item within a List, as well as an iterator which specifies its index. In such cases, you can use a special function called enumerate()
which takes in a list and returns both the item and its index:
fruits = ['apples', 'oranges', 'bananas']
for i, fruit in enumerate(fruits):
print('the ' + fruit + ' are in position ' + str(i))
While the for ... in ...
loop will serve most purposes, there is another kind of loop which will iterate over a piece of code until a certain condition is met:
i = 0
while i < 5:
print(i)
i += 1
In this case, the loop will keep going while its condition is satisfied, and only stop once the variable i
obtains a value greater or equal to 5. This type of loop can be useful if you do not know how long the loop should be run for, or if you want to make the termination criteria somehow dynamic relative to other activities within the script. It requires a bit more setup, however, as the value tested must first be initialized (i = 0), and there has to be code within the loop which changes that value in such a way that it eventually meets the exit criteria. The +=
operator here is a shorthand in Python for adding a value to a variable. You can write the same thing explicitly like:
i = i + 1
This type of while ...
loop is inherently more dangerous than a for ... in ...
loop, because it can easily create a situation where the loop can never exit. In theory, such a loop will run indefinitely, although in practice it will most certainly cause Python to crash. The most dangerous kind of loop is also the simplest:
while True:
print('infinity')
because by definition it has no way to ever terminate. Surprisingly, this kind of while True
loop does have some common uses, but you should never write code like this unless you absolutely know what you are doing (maybe try it just the once so you can get a sense of its effects).
In this guide we've covered the two primary 'flow control' structures of any programming language: conditionals for controlling which code gets executed and loops for repeating a section of code some number of times. With these structures you can start to write much more complex scripts, which are not restricted to executing one command per line, and can exhibit different behavior based on changing conditions in the script.
The following exercise will give you hands on experience working with these two structures by applying them to the attractor point example developed in the previous exercise. This time, we will extend the definition to use conditionals and loops to build out more of the functionality already built in Grasshopper within the Python code. If you don't have it handy you can download the definition from the previous exercise here:
In the last exercise, we saw how the inputs of Grasshopper's Python
component support bringing in data as single items, Lists, or DataTrees by changing the 'Item/List/Tree Access' options in the input's context menu. In the last exercise we used the 'Item Access' option to bring in only a single point and radius value at a time and let Grasshopper handle the iterations over all the points in the set. This time, let's use the 'List Access' option to bring in all the values at once and do the iteration in Python using the loop structures we learned in this section. First change the access type of each of the inputs of the Python
component to List Access
by right clicking on the input and selecting it from the context menu. Then, double-click the Python
component to access the script editing window and input the following code:
import Rhino.Geometry as rh
circles = []
for i in range(len(points)):
point = points[i]
dist = dists[i]
circle = rh.Circle(point, dist)
circles.append(circle)
Since we are now working with Lists of data in both the points
and dists
input, we need to use Python's loop functionality to iterate over each item of data and construct the points. On line 5 we use a combination of Python's built-in range()
and len()
functions to first get the length of the list of points, and then use that length to generate a list of integers starting from 0 to 1 less than the number of items in our points list. Now within our loop body, the variable i
declared in the loop statement will take on the value of this integer. On the next two lines of code, we use the i
variable to pull a single point and single distance from the input lists so we can work with them one at a time.
Once we have one point and one distance we can use them to construct a circle on line 8. Finally, we add the new circle to a list of circles which will be sent back to Grasshopper through the Python
component's output port. Notice that to add items to a list we first have to initialize the circles
variable as an empty list on line 3 and then add each circle to the List using the List's append()
function. Also make sure that you have a Python output also called circles
so the data stored in the variable in Python is made available in the output.
Once you've finished writing the code, click the Test button to run the code and make sure there are no errors. If everything works correctly you should see all components turn white and the same circle pattern with different size circles from the last exercise. Congrats, you've now used loops to handle multiple items in a List directly in Python.
Creating a grid of circles
Now that we're processing the list of points directly in Python, let's go one step further to also incorporate the radius calculation directly in our Python code as well. First, let's remove the dists input from our Python
component—we no longer need the distances since we will be calculating them in Python. Instead, make a new input called attractor
and set it's Type Hint to Point3d
. Finally connect the attractor point stored in the Point
container in Grasshopper to the new input to bring the attractor point into our Python definition. You can keep the access type as 'Item Access' since we only have one attractor point to work with.
Importing the attractor point into Python
Now, open the Python code editor and replace the code with the following:
import Rhino.Geometry as rh
circles = []
for point in points:
dist = point.DistanceTo(attractor)
radius = dist / 5
circle = rh.Circle(point, radius)
circles.append(circle)
Now instead of using the radius calculated in Grasshopper we can calculate it directly in Python by first using the DistanceTo()
method of each point to calculate the distance to the attractor point, and then dividing the distance by a fixed value to get the radius for each circle. Notice that since we no longer need to access data from two separate lists, we no longer need the i
variable to store the index and can use the simpler for ... in ...
loop structure to iterate over the points
list directly. For the final step, we can import the Slider
component value we were using to scale the radii into the Python
component as well so we can use it to dynamically scale the circles. Remember to replace the hardcoded value 5
in the Python code with the input name you used to connect the Slider
component.
Calculating distance and radius inside the Python
component
To give us more control over the size of the circles, let's use a conditional to set the radius directly based on the distance instead of calculating it using the distance value itself. Replace the code in the Python code editor with the following:
import Rhino.Geometry as rh
circles = []
for point in points:
dist = point.DistanceTo(attractor)
if dist < 2:
radius = 0.25
else:
radius = 0.5
circle = rh.Circle(point, radius)
circles.append(circle)
In this code we've replaced the line radius = dist / 5
with a conditional that directly sets the radius to 0.25 if the distance is less that 2 units, and 0.5 otherwise.
Final definition with replaced Grasshopper components disabled
So far we have used a single loop to iterate over all the points that are brought into our script as a list. However, the points and grid are still being generated in Grasshopper. Let's go a step further and replace the full functionality of generating the points inside the Python code.
First, we need to replace the input coming into our Python script from one expecting a list of points to a single numerical parameter which will control the number of points we want to generate in each direction of our grid. You can either rename the existing 'points' input or delete that one and create a new one. If you do reuse the input make sure to reset the input to Item Access
and Type hint to No Type Hint
otherwise you will get an error when connecting the numerical input.
Next, let's modify the code inside our script to generate the grid of points directly. Double-click the Python
component to open the code editor and replace the code with:
import Rhino.Geometry as rh
circles = []
for x in range(num_points):
for y in range(num_points):
point = rh.Point3d(x, y, 0)
dist = point.DistanceTo(attractor)
if dist < 2:
radius = 0.25
else:
radius = 0.5
circle = rh.Circle(point, radius)
circles.append(circle)
Here we have replaced the single for
loop that iterated over the list of points with two nested loops, where each loop iterates over a set of values in each dimension (x and y) of our grid. Then we use Rhino's Point3d
class to create the point object that replaces the point we were previously pulling from the list. Notice we are using the x
and y
values generated by the nested loops to set the x and y coordinates of each point, and a value of 0
for the z value since we are working in the 2-d xy-plane.
+ERROR
If you run the code above you may get an error reading:
Runtime error (TypeErrorException): range() integer end argument expected, got float.
This error means that the
range
function is getting an input with an unexpected type. In this case, the function expects an integer (whole number) but is getting a float (decimal number). This is happening because a numerical value brought in from Grasshopper is by default converted to a float in the Python code. To correct this we can either set the type explicitly at the time the value is brought in by setting the 'Type hint' to 'int' or handle it inside the code by wrapping each use of thenum_points
variable in aint()
function which converts any numerical value to an integer by rounding to the closest whole number.
Let's handle it in our code by changing the two loops to:
for x in range(int(num_points)):
for y in range(int(num_points)):
# ...rest of the code
Now the script should run without error and you should see the same grid of circles from before displayed in the Rhino model. You can also clean up the Grasshopper definition by removing the unused components. We have now replaced all the Grasshopper functionality with Python code and are only using Grasshopper to generate the grid resolution parameter and the attractor point.
+Note
You may have noticed that when we removed the Grasshopper components generating the points we no longer see the points in the Rhino model. This is because while we are now generating the points in the Python code, we are only using them to generate the circles which are then stored in a list and exported out to the Grasshopper canvas. In order to see the points as well we need to place them in their own list and create an output with the same name in the
Python
component. Check the image above for how to do this and try it in your own model.
This concludes our extension of the attractor point tutorial using conditionals and loops to bring more of the functionality from Grasshopper into the Python code. In case you got lost along the way, you can download a finished version of the demo here:
+CHALLENGE 6: Ripple effect
Can you modify the conditional statement in our code to create a 'ripple effect' in the circle grid. The resulting pattern should use three circle sizes based on distance from the attractor, with the largest size being in the middle representing the 'ripple'.
HINT: Remember that conditionals always start with a
if
statement that declares the first condition, after which any number ofelif
statements can add subsequent conditions to test, and finally anelse
statement can be used to catch any case where all previous conditions resulted inFalse
.
In this example, we flatted our points data before passing it into the Python
component so we could bring all the points into our Python code and work with them as a List. Since Grasshopper Lists are converted directly to Python Lists when you set the access type to 'List Access', this is definitely the easiest way of working with multi-item data from Grasshopper in Python. However, what if our data is already in a DataTree structure in Grasshopper and we need to maintain that structure in our script? Can we import the DataTree directly into our Python script and work with it there?
Working with Grasshopper's DataTree structure in Python adds extra complexity and should be avoided if possible. If we only need to work with one value in the Tree at a time we can use 'Item Access' mode and Grasshopper will maintain the same DataTree structure in the output (you can try this by reverting to our 'Item Access' implementation above and getting rid of the 'Flatten' shortcut in the 'Pt' output of the Point
component). You can also flatten the DataTree before inputting it into Python and use the 'List Access' mode to work with it directly as a single Python List as we did above.
However, if you absolutely must deal with the DataTree structure directly in Python, you can do so by changing the input type to 'Tree Access' and bringing the DataTree structure directly into Python. Let’s see how we can work with this data by making some modifications to the attractor point script. Let’s take off the 'Flatten' filter in the Pt
component's output and change the 'points' input of the Python
component to 'Tree Access' mode.
Working with DataTrees in Grasshopper
This will bring the center points into our script as a DataTree with 5 branches of 5 points each. The data is now represented in Python as a special type called 'DataTree' (we can see this by using the type()
function in Python and printing the results).
print type(points)
This data type has several properties and methods that allow you to work with the DataTree structure and access the data in different branches of the Tree.
.BranchCount
property stores the number of branches in the DataTree.Path()
method returns the path of a branch given its index.Branch()
method returns a List of data in a branch given its indexUsing these methods we can modify our script to work directly with the Tree data. First we create a loop to iterate over all the branches in the tree (we use range()
to create a List of indexes from 0 to the number of branches) and loop over them using the variable i
.
for i in range(points.BranchCount):
Then we create a second loop to iterate over each point stored in each branch. Remember that the i
variable is iterating over the index of each branch, so we can use points.Branch(i)
to access the data in each branch one at a time.
for pt in points.Branch(i):
What if we also want to output our results in DataTree format? Again, this adds extra complexity to our script and should be avoided. If we really need to do it though, we can. In this case we need to actually create a new DataTree object, which requires us to import two additional classes from the main Grasshopper library into our script. We can import them by writing these two lines at the top of our script:
from Grasshopper import DataTree
from Grasshopper.Kernel.Data import GH_Path
The DataTree class allows us to create new DataTree objects while the GH_Path class allows us to create path variables which tell the DataTrees where to store data. Both of these classes are found within the main Grasshopper Python library and can be imported using the from ... import ...
syntax to import only the specific classes we need.
Creating DataTrees in Python
Now we need to change the 'circles' output to work as a DataTree instead of as a basic Python List. First we declare 'circles' as an instance of the DataTree
class:
circles = DataTree[object]()
Then inside of the first loop we create a new variable to represent the path to the branch where we will place the data:
for i in range(points.BranchCount):
newPath = GH_Path(i)
The GH_Path class can define any DataTree path by taking in a sequence of integer values. In this case we pass in the i
variable which is storing the index of each incoming branch. This will in effect replicate the structure of the incoming DataTree.
Finally, we use the DataTree's .Add()
method to add each circle to the Tree based on the specified path.
for pt in points.Branch(i):
circles.Add(rh.Circle(pt, .4), newPath)
In this exercise, we looked at how we can use the Rhino.Geometry
library together with the code structures of loops and conditionals to replicate most of the functionalities of the attractor point example we previously developed in Grasshopper directly with Python code. Although working this way takes practice, it gives us a huge degree of control over our geometry and allows us to describe complex computational models beyond the limits of what can be directly done in Grasshopper.