1. Home
  2. Training Library
  3. Pythonic Programming

Pythonic Programming

Developed with
Trivera

Contents

keyboard_tab
Introduction
1
Introduction
PREVIEW48s
Pythonic
2
Pythonic Programming
PREVIEW55m 21s
Review
3

The course is part of this learning path

Pythonic Programming
course-steps 6 certification 1 lab-steps 2
play-arrow
Pythonic Programming
Overview
Transcript
DifficultyBeginner
Duration57m
Students214
Ratings
5/5
star star star star star

Description

Introduction

This training course provides you with a deep dive into the various Python language features that you should be exploiting to write clean, concise, readable, and maintainable code - the Pythonic way!!

Learning Objectives

  • Understand how to write Pythonic code
  • Understand Python specific idioms
  • Understand how to work with key collection data structures such as Tuples, Lists, Sets, and Dictionaries
  • Understand how to work with Iterables and List, Set, and Dictionary Comprehensions
  • Understand Lambda Functions, Generator Functions and Expressions and when and how to use them
  • And finally, understand string formatters and formatting

Prerequisites

  • A basic understanding of the Python programming language
  • A basic understanding of software development
  • A basic understanding of the software development life cycle

Intended Audience

  • Software developers interested in learning how to write Python code in a Pythonic way
  • Python junior level developers interested in advancing their Python skills 
  • Anyone with an interest in Python and how to use Python to write concise and elegant scripts for general purpose tasks

About the Author

Students12383
Labs28
Courses65
Learning paths14

Jeremy is the DevOps Content Lead at Cloud Academy where he specializes in developing technical training documentation for DevOps.

He has a strong background in software engineering, and has been coding with various languages, frameworks, and systems for the past 20+ years. In recent times, Jeremy has been focused on DevOps, Cloud, Security, and Machine Learning.

Jeremy holds professional certifications for both the AWS and GCP cloud platforms.
 

Covered Topics

- [Jeremy] So again let's start with the question of what does it mean to write Pythonic code? Well, writing Pythonic code is where you exploit the language features within Python to write concise, clear, maintainable code. So let's jump into our Terminal and we'll start up our Python 3 interpreter and what we'll do here is we'll demonstrate a piece of code that isn't considered Pythonic. So, let's setup and array and we'll populate it with the numbers one through to 10. And what we wanna do is write some code that squares each of these numbers. And we're going to do it in a manner that doesn't use the Pythonic way of accomplishing this task. So, typically you would probably do something like this in a language like Java or C++. So it would start with a for loop and we would go from zero through the length of the array, and then for each item in the array we want to get the squared value, so we would take the ith item out of the array and we would multiply it by itself. It should be numbers. Okay, so that gives us our squared value. And then we would add that back into the array at the same position, so... Like so. Okay, so now if we take a look at the contents of numbers, we can see that we have achieved what we set out to do, which is to square each item in the array. Now that accomplishes the task but it's not exactly Pythonic. So, let's take a look at how we could achieve the same result but by using the language features of Python itself. So again, we'll reinitialize the array. And this time we'll use a list comprehension whereby we specify for each item in the array we want to square it, and all we do is enter for x in numbers. Enter, apologies, we don't need the comma. So. So what we're saying here is that x is squared for each item in numbers. Enter. So again, if we look at numbers now we can see that we've achieved the same result, but in a much more concise and simpler manner. And this exemplifies the concept of being Pythonic. So in terms of guidelines that you can follow to write your own code that is considered Pythonic, these have been declared in what's referred to as PEP 20. So if you search for PEP 20, PEP stands for Python Enhancement Proposal and PEP 20 is considered the Zen of Python. So when we navigate to PEP 20, we see that this enhancement proposal is the Zen of Python and if we scroll down there are a number of guidelines that if you read through them these will guide you to write what is considered Pythonic code. So we won't go through each of them individually, but do take time to read them and review them and then use them as guidelines for writing your own Python Pythonic code. Another thing that I'll draw your attention to is if we jump back into our Terminal, again we fire up our interpreter. We can actually call up these guidelines within the interpreter. And we do this by doing import this. So again we have the same statements and as mentioned, take time to read them because these will guide you to write what is considered Pythonic code. Okay, let's now move on and talk about tuples. A tuple is a collection of related values or a sequence of related values. Very similar to our list. The values stored in a tuple can be of any type and they are indexed by integers. Now one of the very important differences between a tuple and a list is the fact that a tuple is considered immutable, whereas a list is mutable. To great a tuple you use a comma-separated list of objects. Parenthesis are not needed around a tuple unless the tuple is nested in a larger data structure. So let's now jump back into our Terminal and create and play with tuples. We'll start up our Python 3 interpreter, and we'll create our first tuple. So for example we might create a person tuple and the person will have a name, maybe a first name and maybe a surname. It may also have an age, closing parenthesis, Enter. So now if we call up person again we see that it's a tuple and it has three items in it. The first two are strings. The first being the first name and the second being the surname and the third item in the tuple is an integer or number. If we use the type function on person we can see indeed that it is of type tuple. Now, as previously mentioned you don't always have to use the parenthesis. So we could have created our tuple like so. That is still considered a valid tuple. So again if we look at the type on person, it's still considered a tuple. And if we just call up person we get the same result, but this time we didn't add the age element into the tuple. We can actually create a tuple with exactly one item, but when we do so we need to add a single comma at the end of it. So again if we look at the type it's still considered a tuple. Now if we didn't add the single comma at the end, when we look at the type of this now, we'll see that it's actually considered a string. So that is a gotcha. So if you're creating a tuple with a single value without parenthesis, then it's important to add the single comma at the end of it. So that is really all that's required to create a tuple. Now, while we're here, let's consider some of the other data types that collect a number of items related or not. So the other one is a list and a list is declared like so. So we might have cars and this time we use square brackets. So we might have ford, toyota, ferrari. Okay, so that is a list. So if we do type on cars you can see that it is classified as a list, which is correct. So the main difference between declaring a list and a tuple is with a list you use square brackets, with tuples you either use parenthesis or if you're not using a parenthesis remember to add a single comma after the first item if there's only one element in the tuple. So the other data type that we'll quickly cover in terms of representing multiples of different elements is the idea of a dictionary. So this time we might declare a developer with curly brackets. It has a name... And also has a surname... And has an age. Closing curly bracket. So if we take a look at the type of developer, we can see that it is of type dictionary. So, again what I want to do is to make sure that you understand the differences of declaring each of these different data types. So again let's quickly review the different data types. We have the tuple. So a tuple uses rounded brackets, value one, comma, value two, et cetera. We have a list. It's declared with square brackets. Value one, value two. And then we have our dictionaries or maps which use curly brackets and use a key value mapping data structure. So key, value one. Key one, value one, comma, key two, value two. And so again one of the other important differences between a tuple and a list is that a tuple is read only. it means that you cannot reassign elements within the tuple. So let's try this. So let's take our data tuple and let's grab the first element. So we can see here that the first element in the tuple is value or val1. Likewise, we could grab the second item. Okay, let's now try and update the first element. So we'll go val1-updated. Here we get a TypeError basically telling us that we cannot update the first item in the tuple because tuples are read only. That is not the case with lists. So if we look at a list and we grab the first item out of it, we can also grab the second item out of it. Now this time we can actually update the first element. So this time we'll go val1-updated and that's worked. If we grab that item, you can see that the value has indeed updated, and if we actually call up the dataList variable, you can see that yes, the first item has been updated. So that's one of the key important differences between a tuple and a list. Okay, let's carry on now with our slides. So one of the cool features when working with tuples and lists is the ability to unpack them. So when we want to refer to the individual items within either data type, rather than using an indexer to get the element, we can actually just have the Python language unpack it for us. So let's take a look at this. So we're back within our Python interpreter. Let's reestablish our birthday tuple. Now, as mentioned before, if we wanted to grab the individual items we could use an indexer. So we could grab the month by using the zero representing the first element in the tuple. Likewise we could grab the day and also the year. However, when writing Python code, this doesn't really tell you straight away what the value in the tuple at this position actually represents. So rather than using this approach, we can unpack automatically into variables by doing something like this. Month, day, year equals and then we just refer to our tuple. Now if we look at month we've got month. If we look at day we've go the day value, and likewise with year. So that's unpacking. So it's taking the tuple and unpacking it into each of these variables based on position. So this takes the zeroth position, this takes the next position, this takes the position after that. So that's unpacking and it's a powerful feature within Python and should be used as often as possible. Let's take a look at another example. This time we have got a people variable which is a list of tuples. So again, back within our Python interpreter, we'll establish people to be a list of tuples. So if we look at the type of people, it's a list. And if we look at the type of the first element in the list, that's a tuple. And likewise we can navigate into the elements within the tuple. So if we look at the first item, it's a string as we expect because we've got a string in the first value. Okay, this time we're going to use a for loop and we'll do first underscore name, last underscore name and org for organization within the people list. And then we'll simply print out the first_name and the last_name, Enter and there we go. So here are the results. So, what have we achieved here? We've setup a list of tuples and then we've used unpacking to take each of the values out of the tuple and assign them to each of these variables. Then within our print statement we've simply printed out the first_name and last_name. So again, a very powerful feature of Python and very much a Pythonic way of doing this. So the next thing we'll move onto is the concept of unpacking function arguments. So, sometimes you need the other end of iterable packing. That is, what do you do if you have a list of three values and you want to pass them to a method that expects three positional arguments. So, one approach is to use the individual items by index as previously explained, or we could consider a more Pythonic approach, which is to use the asterisks symbol to unpack the iterable into individual items. So let's take a look at this. So we'll take the following code and we'll jump back into our interpreter. So back within our Terminal we start the Python 3 interpreter and this time we'll declare a people list and the list will contain tuples, where each tuple represents a particular person. Again, we can index this so we can look at the first tuple, likewise the second and the third. Okay, what we'll do now is we'll define a function called person underscore record, and that particular function will take four input parameters. The first being first_name, the next being last_name, followed by city and then followed by state. The function then uses those parameters in a string format to print out a string to the screen. Okay, so now what we'll do is we'll iterate using a for loop over our people list. And for each person, well person tuple in that list, we'll call our function. So here's the magic. We use the asterisk symbol to unpack the tuple as it's passed into the person underscore record function. And there we go. Here we have the output. So we have one line for each item in the list, where each item is made up of a tuple; first name, sur name, city and state. Okay, let's carry on now with our slides. So often when you're working with collections, either tuples or lists, a key requirement is to sort that particular collection. Within Python, the sorted builtin function can be used to return a sorted copy of its argument, which can be any iterable. You can customize the sorted function with a key parameter. So let's take a look at this. So we'll take this example and we'll jump back into our interpreter. We'll establish our fruit list. So this is a list of fruits where each fruit is a string. And this time we'll create a new variable called sorted_fruit and we'll call the inbuilt sorted function and pass in our unsorted fruit list. Okay, we can now take a look at the sorted fruit and here we can see that the ordering has indeed been changed. So we have Apple, followed by BLUEberry, followed by FIG. Note that this is a case sensitive ordering. So capital A, followed by capital B, followed by capital F. Further on down the list for example, we'll find apricot which begins with a lowercase a, followed by banana which begins with a lowercase b. Okay, let's carry on now with our slides. Let's now consider custom sort keys. You can specify a function with the key parameter of the sorted function. This function will be used once for each element of the list being sorted to provide the comparison value. For example, you could provide a custom function that sorts a list of strings case and sensitively or another one that sorts a list of zip codes by the number of Starbucks within the zip code. These are just some examples of different ways of sorting your lists or tuples. Let's take a look at this now. So again, back within our Python interpreter we'll establish our fruit list. And this time we'll define a new function called ignore underscore case. We'll take a parameter which will be each item that we pass into it and we'll return the item, but in lowercase. Okay, so this time we can grab our sorted_fruit. We'll call it sorted_fruit2 and we'll set it to be sorted, pass in our list and this time we specify a key, which is our custom sorting function. Which is ignore underscore case, Enter. So now if we call upon our sorted underscore fruit2, we'll see that the ordering is slightly different compared to the previous version. So we look at the previous version, the first element was Apple, second element was BLUEberry. This time with our custom sort function, the first element is Apple, followed by apricot, which is leveraging our ignore_case function. Okay, let's carry on now with our slides. So let's now consider another custom example of ordering our fruit list. We'll do so by length of each name and then by the name itself. So, within our interpreter we define our by_length_then_name function. We'll then establish our new sorted_fruit3 list to be sorted again passing in our fruit list and this time our new sorting function, which is by underscore length then by name. Okay, if we take a look at that sorted underscore fruit list, you can see that indeed we are first sorting by the length of each name because here FIG is a three character name, followed by date which is four, Kiwi is also a four length string, but K comes after D. So what you can see here is that there's great flexibility in terms of doing custom sorting on our lists and tuples. Let's now consider an entirely different example. This time we'll setup nums list with a list of numbers. Then we'll just do a default sorting on this particular list. Now if we call up n1 you can see that this list has been ordered by the numerical value. Now this time we'll create n2, which uses the sorted function, but instead sets the key to be the data type string. Now if we look at n2, you can see that the list has been ordered based on the string value of each of those items. Okay in this last sorting example we're going to sort a list of books using a custom sort function that leverages a regular expression to ignore the leading the's a's or an's in the title. So let's jump into our interactive Python interpreter, and the first thing we'll do is we'll import re, the regular expression module. We'll then establish our list of books. So if we look at rebooks. So what we wanna do is we wanna sort this list ignoring any leading a's or the's. And so we establish and compile our regular expression. In this case we're searching for leading the's a's and an's, and we'll remove them. We then define our custom sort function. In this case we're passing in the title, we're using our compiled regular expression to strip out the the's, the a's add the an's and then we lowercase the title. Okay, so finally what we have to do is do a for loop on the book in sorted, we pass in our books, and we set our custom sort function, which is strip_articles. And all we'll do now is we'll just print out each book. And there we go. So, ignoring the the's, we can see that the first item comes out begins with Adventures, followed by Case, then His, followed by Hound, followed by Memoirs, followed by Return, followed by Sign, Study and Valley. So that's correct. So again you can see the power in creating custom sort functions. Okay the next important thing we're going to look at in terms of Pythonic programming is the concept of lambda functions. A lambda function is a brief function or a function definition that makes it easy to create a function on the fly. This can be useful for passing functions into other functions which will be called later. Functions passed in this way are referred to as callbacks. You'll often hear about callbacks in other languages. For example normal functions can be callbacks as well. One important use of lambda functions is for providing sort keys and another is to provide event handlers in GUI programming. The basic syntax for creating a lambda function is the lambda keyword followed by a parameter list, colon, and then an expression. The parameter list a list of function parameters and the expression is an expression involving those parameters. The expression is the return value of the function. So let's take a quick look at how we create lambda functions. So we'll create our first one, we'll call it addFive. We specify that it's a lambda and that it has a single input parameter and what we're going to do is take that input and add five to it. So, it's as simple as that. We can now call that lambda function and we pass in a value, we'll pass in two and when we add five to it, we get seven. So we get the right result. Again, we could call it with 100 and get 105. Okay, so it's as easy as that. We could create another lambda function as an example, add two numbers. Specify that it's a lambda and this time it takes two imports, x and y. And all we're going to do here is add them together. Again, we can now call this new lambda function, and we'll pass in 20 and 40. And we get the result 60, which is adding the two numbers together. Now, the real power with using lambda functions is when you start to pass functions into another function. So let's show you an example of this. Let's setup a new lambda function called printResult. Again, it's a lambda and it's going to take in two parameters. The first is a function and the second is a value, and then what we're going to do is print the execution of that function over that value. Okay, so we've created it. So let's now call it. So we'll go printResult and the first parameter that was pass in another lambda function. In this case we'll pass in addFive and then we will pass in the value that we want to operate on it. So let's pass in 15. Pressing Enter, we get the correct result which is 20. Which is adding five to our value, 15. Okay, jumping back to our slides. Let's now see how we can use lambdas directly within other inbuilt functions, such as the sorted function. We'll jump back to our Python interpreter, we'll establish a list of fruits. If we take a look at our fruits, we have the ordering as declared when we initialized the list. And this time we'll created sorted underscore fruits, we'll use the sorted function, we'll pass in our unordered fruits list and here we'll specify the key to be a lambda function. So we specify lambda, e for element and then we'll call the lower on each element, and it's as simple as that. So now we call sorted_fruits, we can see that our ordering has changed as expected. So this shows you how lambda functions can be used in different contexts within Python to write clear and concise code. Okay, we'll move on now to list comprehensions. Okay so, list comprehensions. A list comprehension is a Python idiom that creates a shortcut for a for loop. It returns a copy of a list with every element transformed through an expression. Functional programmers refer to this as a mapping function. So let's take a look at this. So as an example, we might have a numbers list with the following numbers, one through to 10. Now, what we can do is we could square all of those numbers by doing the following. X times times two for a square, for x in numbers. It's as simple as that. So, that is a list comprehension. If we look at squared now, you can see that each number within our original numbers list has been squared. As another example, we might want to establish an evens list which contains the first 20 even numbers. Now to do this we could again use a list comprehension. In this case we'll go x times two for x in range, we'll go up to 20, Enter. Now if we have a look at evens you can see that we've got the first 20 even numbers. So as you have just seen, list comprehension is a very convenient way to construct less dynamically using very little code. So let's take a look at these examples again which use list comprehensions. So for the first one, again we'll use our fruits list, and this time we're going to uppercase all of our fruits into a new list. We do so by calling fruit.upper for fruit in our fruits list. Okay, now if we look at our new list, we can see that each element has been upper cased. Again just using a simple list comprehension. In our next example we'll have a values list which contains numbers, strings and another list. So a list within a list. Okay, so if we have a look at values. Now what we're going to do is double each of the elements. So we'll setup our new list called doubles is equal to v times two for each value in our values list. So the interesting thing here will be the treatment of multiplying two on a string, as well as on a embedded list. So let's have a look to see what's happened. So here we can see that the numbers have been multiplied by two and for our string we simply repeat the string twice, boomboom and likewise for our list. We're just repeating each entry twice. So a, a, b, b. Okay, so that's list comprehensions. Nice and simply but very powerful and very expressive. Okay, moving on to dictionary comprehensions. A dictionary comprehension has syntax very similar to a list comprehension. The expression is a key value pair and is added to the resulting dictionary. If a key is used more than once it overrides any previous keys. This can be handy for building a dictionary from a sequence of values. Okay, let's take a look at an example of this. Okay this time we'll establish an animals list with the following entries. And this time we'll establish a new comprehension with curly brackets, a.lower, colon, and then we'll go for the length of a for a in animals. Closing curly bracket. Okay so let's now take a look at our new dictionary and we can see that it has been generated as per our dictionary comprehension. Similarly to using comprehensions to generate lists and dictionaries, we can also use them to generate sets. So a set comprehension is useful for turning any sequence into a set. Items can be modified or skipped as the set is being built. So let's see an example of this. So we're going to take this code here, which will open a file off the file system and then it will loop through each line and then do a split on the words in that line and add them to the set. So that at the end of it, we get a set of unique words taken out of the file from the file system. So let's try this out. So to start off with we need some text to add into our file that will save on the file system. So we'll navigate to this wiki article and we'll just borrow the poem Mary Had a Little Lamb. So we'll grab the text here, copy, then we'll jump into our Terminal, and we'll write this out to a file called mary.txt, and paste it and save it. So if we do a directory listing and then we cat out the file we should have our text, which we do. So we'll now write our Python code which will read this file. So, we jump into our Python 3 interpreter. We import regular expression module. We then want to open our file that we just created. It's in the current directory mary.txt as mary_in. Okay, with this we're then going to create our set comprehension. So s equals curly brackets for set, for each word that we discover we want to lowercase it. Now this is where we need to loop through each line within the file. So for each line in mary_in, which is the file, then for each word in and then we'll use a regular expression and we'll split on white space. So we go w plus and we do it on each and every line. And then finally we'll also add in an if clause to ensure that we only do this on words. So we're not capturing any white space. Okay, so that is it. Now if we have a look at s, it should be populated with only unique words out of that file, which it has. So this is a great result. So this shows you again the power of using comprehension and in this case to generate a set. The set being a set of unique words captured out of our mary.txt file. Okay we'll now move on to iterables. So in Python an iterable is anything that can be looped over. Python has many builtin iterables. For example a file object allows iterating through the lines in a file. All builtin collections, lists, tuples, strings, bytes, also are considered iterables. They keep all their values in memory. Now another form of an iterable is a generator. A generator does not keep its values in memory and we'll go into this in greater detail in the coming slides. Now as can be seen here, iterables can be classified into either collections or generators. Collections are stored in memory and are considered eager. That is everything is available at once in memory. On the other hand you have generators which are considered virtual and lazy. That is the data that is generated is not stored in memory and is only available when requested. We're going to go much deeper into the concept of generators in the following slides. As just mentioned, we have the concept of generators and with generators we can use generator expressions. A generator expression is similar to a list comprehension, but it provides a generator instead of a list. That is while a list comprehension returns a complete list, a generator expression returns one item at a time. The main difference in syntax is that the generator expression uses parenthesis rather than brackets. Generator expressions are especially useful with functions like sum, min and max that reduce an iterable input to a single value. Okay, let's take a look at generator expressions. Now in the example that we're going to use here, we're going to sum the squares of the numbers zero through to nine. We'll first do so using a list comprehension and then secondly we'll do the same thing using a generator expression. So for our first example we're going to use the sum function and we're going to create a list inside the sum function. We're going to use a list comprehension where we square x for x in the range 10. Enter. So what have we just done here? So we've used a list comprehension to generate a list. So if we look at this by itself we can see that it generates the numbers zero through to nine squared. And then we're summing it. So the sum of those squares is 285. Now, the key point here is that that list was generated and stored in memory all at once. So let's do the same thing now with a generator expression. So s2 equals, we use the sum function again, but this time we're not going to use square brackets to indicate that we're creating a list. Instead we put the expression straight in... Using range 10 again, Enter. Now if we look at s2, we get the same value, but the difference here is that it used a generator expression which means it only calculated the square of each number and stored it once as and when requested. So we didn't store the whole list in memory. The key point from this is that it is memory efficient to use generator expressions as opposed to using list comprehensions. Okay, let's take a look at another example when generator expressions can be extremely useful for memory management. In this example we're going to read in some data stored in a file on the file system. But we're only going to store each line once in memory before the next line is read and stored in memory. Okay, we'll jump into our terminal. We'll do a directory listing and we've still got our mary.txt file that we used in a previous example. If we look at the contents it contains the Mary Had a Little Lamb poem. So, we now fire up our Python 3 interpreter, and what we'll do now is we'll create a page variable and we'll open our file. So what we're trying to achieve in this example is to discover the maximum length of a line within that file. So, we'll setup m to be the max. And we use the length function of each line in the page file. Okay, we then close our file. And if we take a look at m we can see that the result is 37. So to summarize what we've just done, we opened the file, we looped through it line by line, reading one line at a time into memory and we check the length of each line and then we filtered to find the maximum length, which was 37. Similarly to generator expressions, we also have generator functions. A generator function is much like a normal function in Python, except that instead of using a return statement, it has a yield statement. Each time the yield statement is reached, it provides the next value in the sequence. When there are no more values the function calls return and the loop stops. A generator function maintains state between calls, unlike a normal function. This is very important and what we'll do now is we'll demonstrate this. Back within our Terminal we start up our Python 3 interpreter and we'll define a new generator function called numbers. And we'll have a local variable initialized to zero. We'll then have a for loop where we loop through the numbers zero through to two. And then we'll call yield and we'll return a. We'll then increase a each time by one. We'll then have another for loop, again using the same range. Again we'll yield a, but this time we'll subtract one each time through the loop. And then we'll just add in a couple of arbitrary yields where we just return static values. 100, 200 and 300, just for example. Okay, so that's our generator function. Now the key point in there is that there is some state within that function and that state is maintained as we generate and return or yield values from that function. So what we'll do next is we'll use another for loop and we'll iterate over our generator function. Like so. And then we'll just print out each num. Okay, so let's have a look. And there we go. So you can see here that at the start we go up zero, one, two, three, and then we come down three, two, one, and then finally we call 100, 200 and 300. So that shows you how to use a generator function with the yield statement and that really it's not a lot different to a normal Python function. So let's now take a look at an example that uses a generator function to calculate all the prime numbers up to a particular limit. We won't go into the specifics of the algorithm, but other than to focus on the fact that it is a generator function where each successive prime number is yielded back to the caller. Back within our Python interpreter we'll enter our generator function, which is called next_prime. It takes a single parameter which is the limit. So this function is going to derive and return prime numbers up to a particular limit. So what we can do next is we can do a for prime in our function that we've just created and in this case we'll go up to 200 and then what we'll do is we'll concatenate each prime as it's returned from the generator function to our console. And there we go. So we have a list of all of the prime numbers up to our limit that we specified. We can quickly rerun this up to 2,000. And again, we are able to generate quickly all of the prime numbers up to our limit, which was 2,000. The key point here is that we don't pre-generate all of the prime numbers and store them in memory all at once. When we're iterating through our generator function, we just generate the next prime number and return that. So this is very memory efficient. In our next example, we'll again define a custom generator function, this guy's called trimmed. What trimmed will do is it will read in a file line by line and it will trim the line endings. In this example, we'll reuse our mary.txt file. So again if we cat out the contents of this file, we can see that it contains the Mary Had a Little Lamb poem. So, we'll start up our Python interpreter, and then we'll paste in our custom generator function called trimmed. So what this does is it takes a file name as an input, it then opens the file, it then reads in line by line, it does some manipulation on each line, and then finally it yields the line back to the caller. So how would we call this generator function? So we would do a for loop, trimmedline and then we call our trimmed function with the path of our file. And then we'll simply print out each trimmed line as it's yielded from our custom generator function. So print trimmed line, Enter. And there we go. So again, in summary, we created a custom trimmed generator function which read in a line by line into memory, so only one line at a time. It then did some manipulation on that line and then yielded the result back to the caller, which in our case we took and printed to the screen. So moving on, we'll now take a look at string formatting. Within Python there are a number of features that can be use to do string formatting. The traditional way to format strings in Python was to use the percent operator in a format string containing fields designated with percent signs. However, the new and improved method of string formatting uses the format method. It takes a format string and one or more arguments. The format strings contain placeholders which consist of curly braces, which may contain formatting details. This new method has much more flexibility. As mentioned, formatting information can be added. It's preceded by a colon. Here we can see different formatting options. Placeholders can be manually numbered. This is handy when you need to use a format method parameter more than once. So let's try some examples of string formatting. So we'll jump over into our Terminal and we'll start our Python 3 interpreter and then the first example we'll try is the following. So what we're doing here is we're using numbered placeholders and we're going to use the format function to inject the string into these placeholders. Executing we can see that indeed we get the right result, so penguin has been injected correctly into each of those positions. Okay the next one we'll try is, we'll setup a couple of variables. The first one color, blue for example. I'll create an animal variable, set it to iguana. And then we'll show how that we can use auto numbered placeholders. So in this case, we'll enter our placeholders without numbers. We'll call format and then we'll pass in both color, and animal variables. And again we get the right result. So color goes into the first placeholder, animal goes into the second place holder. So, auto numbered placeholders. In our next example we'll create a Fahrenheit variable. Set it to 98 point some random value, and this time we'll use a formatting directive. So our placeholder, we have a colon .1f, .format, pass in our variable, Enter. The .1f means that the format is floating point with one decimal place. Okay in the next example we'll have a value equal to 12345 and this time we'll print out the following where we're using placeholders that are manually numbered and are reusing the same value, but using different formatting directives. So here we have the value 12345 in decimal format, hexadecimal format, octal format and binary format. Okay in our last string formatting example let's setup a data dictionary with the following values. We'll then use a for loop to get each key and value after it's been sorted, and then we'll print out the following where we're using a formatting directive 4d, which means to format the decimal integer in a field of four characters wide. Enter. As you can see we've achieved the right formatting result. Okay in terms of string formatting, the final thing we'll talk about is the concept of f-strings. F-strings are a new feature added to Python 3.6. These are strings that contain placeholders. As used with normal string formatting, but the expression to be formatted is also placed in the placeholder itself. This makes formatting strings much more readable, with less typing. As with formatted strings, any expression can be formatted. F-strings are only available in Python 3.6 and later. Okay, let's take a look at how we use f-strings. So we'll jump back into our Python interpreter, and we'll specify the following variables. We'll look at how we used formatting options before f-strings were available. So here we're taking the name and company variables and we're using auto placeholders and we're injecting them into these locations. So we get the right result. Likewise, we could do the following for x and y, where we're injecting x and y as floating point numbers with two decimal places. Okay and we get the right result. So that was pre f-strings. Let's now take a look at how we would use f-strings to achieve the same result. So with f-strings you can simply inject the variables directly within the string itself. We get the same result. Likewise, we would do the following for our x and y variables. Again, we get the same result. So as you can see, f-strings achieves the same formatting results, but in a much simpler and more concise manner.