<jupyter_start><jupyter_text>Strings Strings are used in Python to record text information, such as names. Strings in Python are actually a *sequence*, which basically means Python keeps track of every element in the string as a sequence. For example, Python understands the string "hello' to be a sequence of letters in a specific order. This means we will be able to use indexing to grab particular letters (like the first letter, or the last letter).This idea of a sequence is an important one in Python and we will touch upon it later on in the future.In this lecture we'll learn about the following:    1.) Creating Strings    2.) Printing Strings    3.) String Properties    4.) String Methods    5.) Print Formatting  Creating a StringTo create a string in Python you need to use either single quotes or double quotes. For example:<jupyter_code># Single word
'hello'
# Entire phrase 
'This is also a string'
# We can also use double quote
"String built with double quotes"
# Be careful with quotes!
' I'm using single quotes, but this will create an error'<jupyter_output><empty_output><jupyter_text>The reason for the error above is because the single quote in I'm stopped the string. You can use combinations of double and single quotes to get the complete statement.<jupyter_code>"Now I'm ready to use the single quotes inside a string!"<jupyter_output><empty_output><jupyter_text>Now let's learn about printing strings!  Printing a StringIn a Jupyter Notebook, just a string in a cell will automatically output strings, but the correct way to display strings in your output is by using a print function.<jupyter_code># We can simply declare a string
'Hello World'
# Note that we can't output multiple strings this way
'Hello World 1'
'Hello World 2'<jupyter_output><empty_output><jupyter_text>We can use a print statement to print a string.<jupyter_code>print('Hello World 1')
print('Hello World 2')
print('Use \n to print a new line')
print('\n')
print('See what I mean?')<jupyter_output><empty_output><jupyter_text>String Basics We can also use a function called len() to check the length of a string!<jupyter_code>len('Hello World')<jupyter_output><empty_output><jupyter_text>Python's built-in len() function counts all of the characters in the string, including spaces and punctuation.<jupyter_code># Assign s as a string
s = 'Hello World'
#Check
s
# Print the object
print(s)<jupyter_output><empty_output><jupyter_text>Let's start indexing!<jupyter_code># Show first element (in this case a letter)
s[0]
s[1]
s[2]<jupyter_output><empty_output><jupyter_text>We can use a : to perform *slicing* which grabs everything up to a designated point. For example:<jupyter_code># Grab everything past the first term all the way to the length of s which is len(s)
s[1:]
# Note that there is no change to the original s
s
# Grab everything UP TO the 3rd index
s[:3]<jupyter_output><empty_output><jupyter_text>Note the above slicing. Here we're telling Python to grab everything from 0 up to 3. It doesn't include the 3rd index. You'll notice this a lot in Python, where statements and are usually in the context of "up to, but not including".<jupyter_code>#Everything
s[:]<jupyter_output><empty_output><jupyter_text>We can also use negative indexing to go backwards.<jupyter_code># Last letter (one index behind 0 so it loops back around)
s[-1]
# Grab everything but the last letter
s[:-1]<jupyter_output><empty_output><jupyter_text>We can also use index and slice notation to grab elements of a sequence by a specified step size (the default is 1). For instance we can use two colons in a row and then a number specifying the frequency to grab elements. For example:<jupyter_code># Grab everything, but go in steps size of 1
s[::1]
# Grab everything, but go in step sizes of 2
s[::2]
# We can use this to print a string backwards
s[::-1]<jupyter_output><empty_output><jupyter_text>String PropertiesIt's important to note that strings have an important property known as *immutability*. This means that once a string is created, the elements within it can not be changed or replaced. For example:<jupyter_code>s
# Let's try to change the first letter to 'x'
s[0] = 'x'<jupyter_output><empty_output><jupyter_text>Notice how the error tells us directly what we can't do, change the item assignment!Something we *can* do is concatenate strings!<jupyter_code>s
# Concatenate strings!
s + ' concatenate me!'
# We can reassign s completely though!
s = s + ' concatenate me!'
print(s)
s<jupyter_output><empty_output><jupyter_text>We can use the multiplication symbol to create repetition!<jupyter_code>letter = 'z'
letter*10<jupyter_output><empty_output><jupyter_text>Basic Built-in String methodsObjects in Python usually have built-in methods. These methods are functions inside the object (we will learn about these in much more depth later) that can perform actions or commands on the object itself.We call methods with a period and then the method name. Methods are in the form:object.method(parameters)Where parameters are extra arguments we can pass into the method. Don't worry if the details don't make 100% sense right now. Later on we will be creating our own objects and functions!Here are some examples of built-in methods in strings:<jupyter_code>s
# Upper Case a string
s.upper()
# Lower case
s.lower()
# Split a string by blank space (this is the default)
s.split()
# Split by a specific element (doesn't include the element that was split on)
s.split('W')<jupyter_output><empty_output><jupyter_text>There are many more methods than the ones covered here. Visit the Advanced String section to find out more!  Print FormattingWe can use the .format() method to add formatted objects to printed string statements. The easiest way to show this is through an example:<jupyter_code>'Insert another string with curly brackets: {}'.format('The inserted string')<jupyter_output><empty_output><jupyter_text>ListsEarlier when discussing strings we introduced the concept of a *sequence* in Python. Lists can be thought of the most general version of a *sequence* in Python. Unlike strings, they are mutable, meaning the elements inside a list can be changed!In this section we will learn about:        1.) Creating lists    2.) Basic List Methods    3.) Nesting Lists    4.) Introduction to List ComprehensionsLists are constructed with brackets [] and commas separating every element in the list.Let's go ahead and see how we can construct lists!<jupyter_code># Assign a list to an variable named my_list
my_list = [1,2,3]<jupyter_output><empty_output><jupyter_text>We just created a list of integers, but lists can actually hold different object types. For example:<jupyter_code>my_list = ['A string',23,100.232,'o']<jupyter_output><empty_output><jupyter_text>Just like strings, the len() function will tell you how many items are in the sequence of the list.<jupyter_code>len(my_list)
my_list = ['one','two','three',4,5]
# Grab element at index 0
my_list[0]
# Grab index 1 and everything past it
my_list[1:]
# Grab everything UP TO index 3
my_list[:3]<jupyter_output><empty_output><jupyter_text>We can also use + to concatenate lists, just like we did for strings.<jupyter_code>my_list + ['new item']<jupyter_output><empty_output><jupyter_text>Note: This doesn't actually change the original list!<jupyter_code>my_list<jupyter_output><empty_output><jupyter_text>You would have to reassign the list to make the change permanent.<jupyter_code># Reassign
my_list = my_list + ['add new item permanently']
my_list<jupyter_output><empty_output><jupyter_text>We can also use the * for a duplication method similar to strings:<jupyter_code># Make the list double
my_list * 2
# Again doubling not permanent
my_list<jupyter_output><empty_output><jupyter_text>Basic List MethodsIf you are familiar with another programming language, you might start to draw parallels between arrays in another language and lists in Python. Lists in Python however, tend to be more flexible than arrays in other languages for a two good reasons: they have no fixed size (meaning we don't have to specify how big a list will be), and they have no fixed type constraint (like we've seen above).Let's go ahead and explore some more special methods for lists:<jupyter_code># Create a new list
list1 = [1,2,3]<jupyter_output><empty_output><jupyter_text>Use the **append** method to permanently add an item to the end of a list:<jupyter_code># Append
list1.append('append me!')
# Show
list1<jupyter_output><empty_output><jupyter_text>Use **pop** to "pop off" an item from the list. By default pop takes off the last index, but you can also specify which index to pop off. Let's see an example:<jupyter_code># Pop off the 0 indexed item
list1.pop(0)
# Show
list1
# Assign the popped element, remember default popped index is -1
popped_item = list1.pop()
popped_item
# Show remaining list
list1<jupyter_output><empty_output><jupyter_text>It should also be noted that lists indexing will return an error if there is no element at that index. For example:<jupyter_code>list1[100]<jupyter_output><empty_output><jupyter_text>We can use the **sort** method and the **reverse** methods to also effect your lists:<jupyter_code>new_list = ['a','e','x','b','c']
#Show
new_list
# Use reverse to reverse order (this is permanent!)
new_list.reverse()
new_list
# Use sort to sort the list (in this case alphabetical order, but for numbers it will go ascending)
new_list.sort()
new_list<jupyter_output><empty_output><jupyter_text>Nesting ListsA great feature of of Python data structures is that they support *nesting*. This means we can have data structures within data structures. For example: A list inside a list.Let's see how this works!<jupyter_code># Let's make three lists
lst_1=[1,2,3]
lst_2=[4,5,6]
lst_3=[7,8,9]

# Make a list of lists to form a matrix
matrix = [lst_1,lst_2,lst_3]
# Show
matrix<jupyter_output><empty_output><jupyter_text>We can again use indexing to grab elements, but now there are two levels for the index. The items in the matrix object, and then the items inside that list!<jupyter_code># Grab first item in matrix object
matrix[0]
# Grab first item of the first item in the matrix object
matrix[0][0]<jupyter_output><empty_output><jupyter_text>List ComprehensionsPython has an advanced feature called list comprehensions. They allow for quick construction of lists. To fully understand list comprehensions we need to understand for loops. So don't worry if you don't completely understand this section, and feel free to just skip it since we will return to this topic later.But in case you want to know now, here are a few examples!<jupyter_code># Build a list comprehension by deconstructing a for loop within a []
first_col = [row[0] for row in matrix]
first_col<jupyter_output><empty_output><jupyter_text>We used a list comprehension here to grab the first element of every row in the matrix object. We will cover this in much more detail later on!  TuplesIn Python tuples are very similar to lists, however, unlike lists they are *immutable* meaning they can not be changed. You would use tuples to present things that shouldn't be changed, such as days of the week, or dates on a calendar. In this section, we will get a brief overview of the following:    1.) Constructing Tuples    2.) Basic Tuple Methods    3.) Immutability    4.) When to Use TuplesYou'll have an intuition of how to use tuples based on what you've learned about lists. We can treat them very similarly with the major distinction being that tuples are immutable. Constructing TuplesThe construction of a tuples use () with elements separated by commas. For example:<jupyter_code># Create a tuple
t = (1,2,3)
# Check len just like a list
len(t)
# Can also mix object types
t = ('one',2)

# Show
t
# Use indexing just like we did in lists
t[0]
# Slicing just like a list
t[-1]<jupyter_output><empty_output><jupyter_text>Basic Tuple MethodsTuples have built-in methods, but not as many as lists do. Let's look at two of them:<jupyter_code># Use .index to enter a value and return the index
t.index('one')
# Use .count to count the number of times a value appears
t.count('one')<jupyter_output><empty_output><jupyter_text>ImmutabilityIt can't be stressed enough that tuples are immutable. To drive that point home:<jupyter_code>t[0]= 'change'<jupyter_output><empty_output><jupyter_text>Because of this immutability, tuples can't grow. Once a tuple is made we can not add to it.<jupyter_code>t.append('nope')<jupyter_output><empty_output><jupyter_text>When to use TuplesYou may be wondering, "Why bother using tuples when they have fewer available methods?" To be honest, tuples are not used as often as lists in programming, but are used when immutability is necessary. If in your program you are passing around an object and need to make sure it does not get changed, then a tuple becomes your solution. It provides a convenient source of data integrity.You should now be able to create and use tuples in your programming as well as have an understanding of their immutability.Up next Sets and Booleans!!  DictionariesWe've been learning about *sequences* in Python but now we're going to switch gears and learn about *mappings* in Python. If you're familiar with other languages you can think of these Dictionaries as hash tables. This section will serve as a brief introduction to dictionaries and consist of:    1.) Constructing a Dictionary    2.) Accessing objects from a dictionary    3.) Nesting Dictionaries    4.) Basic Dictionary MethodsSo what are mappings? Mappings are a collection of objects that are stored by a *key*, unlike a sequence that stored objects by their relative position. This is an important distinction, since mappings won't retain order since they have objects defined by a key.A Python dictionary consists of a key and then an associated value. That value can be almost any Python object. Constructing a DictionaryLet's see how we can construct dictionaries to get a better understanding of how they work!<jupyter_code># Make a dictionary with {} and : to signify a key and a value
my_dict = {'key1':'value1','key2':'value2'}
# Call values by their key
my_dict['key2']<jupyter_output><empty_output><jupyter_text>Its important to note that dictionaries are very flexible in the data types they can hold. For example:<jupyter_code>my_dict = {'key1':123,'key2':[12,23,33],'key3':['item0','item1','item2']}
# Let's call items from the dictionary
my_dict['key3']
# Can call an index on that value
my_dict['key3'][0]
# Can then even call methods on that value
my_dict['key3'][0].upper()<jupyter_output><empty_output><jupyter_text>We can affect the values of a key as well. For instance:<jupyter_code>my_dict['key1']
# Subtract 123 from the value
my_dict['key1'] = my_dict['key1'] - 123
#Check
my_dict['key1']<jupyter_output><empty_output><jupyter_text>A quick note, Python has a built-in method of doing a self subtraction or addition (or multiplication or division). We could have also used += or -= for the above statement. For example:<jupyter_code># Set the object equal to itself minus 123 
my_dict['key1'] -= 123
my_dict['key1']<jupyter_output><empty_output><jupyter_text>We can also create keys by assignment. For instance if we started off with an empty dictionary, we could continually add to it:<jupyter_code># Create a new dictionary
d = {}
# Create a new key through assignment
d['animal'] = 'Dog'
# Can do this with any object
d['answer'] = 42
#Show
d<jupyter_output><empty_output><jupyter_text>Nesting with DictionariesHopefully you're starting to see how powerful Python is with its flexibility of nesting objects and calling methods on them. Let's see a dictionary nested inside a dictionary:<jupyter_code># Dictionary nested inside a dictionary nested inside a dictionary
d = {'key1':{'nestkey':{'subnestkey':'value'}}}<jupyter_output><empty_output><jupyter_text>Wow! That's a quite the inception of dictionaries! Let's see how we can grab that value:<jupyter_code># Keep calling the keys
d['key1']['nestkey']['subnestkey']<jupyter_output><empty_output><jupyter_text>A few Dictionary MethodsThere are a few methods we can call on a dictionary. Let's get a quick introduction to a few of them:<jupyter_code># Create a typical dictionary
d = {'key1':1,'key2':2,'key3':3}
# Method to return a list of all keys 
d.keys()
# Method to grab all values
d.values()
# Method to return tuples of all items  (we'll learn about tuples soon)
d.items()<jupyter_output><empty_output><jupyter_text>Hopefully you now have a good basic understanding how to construct dictionaries. There's a lot more to go into but we won't do that here. After this section all you need to know is how to create a dictionary and how to retrieve values from it.  Functions Introduction to FunctionsHere, we will explain what a function is in Python and how to create one. Functions will be one of our main building blocks when we construct larger and larger amounts of code to solve problems.**So what is a function?**Formally, a function is a useful device that groups together a set of statements so they can be run more than once. They can also let us specify parameters that can serve as inputs to the functions.On a more fundamental level, functions allow us to not have to repeatedly write the same code again and again. If you remember back to the lessons on strings and lists, remember that we used a function len() to get the length of a string. Since checking the length of a sequence is a common task you would want to write a function that can do this repeatedly at command.Functions will be one of most basic levels of reusing code in Python, and it will also allow us to start thinking of program design.  def StatementsLet's see how to build out a function's syntax in Python. It has the following form:<jupyter_code>def name_of_function(arg1,arg2):
    '''
    This is where the function's Document String (docstring) goes
    '''
    # Do stuff here
    # Return desired result<jupyter_output><empty_output><jupyter_text>We begin with def then a space followed by the name of the function. Try to keep names relevant, for example len() is a good name for a length() function. Also be careful with names, you wouldn't want to call a function the same name as a [built-in function in Python](https://docs.python.org/2/library/functions.html) (such as len).Next come a pair of parentheses with a number of arguments separated by a comma. These arguments are the inputs for your function. You'll be able to use these inputs in your function and reference them. After this you put a colon.Now here is the important step, you must indent to begin the code inside your function correctly. Python makes use of *whitespace* to organize code. Lots of other programing languages do not do this, so keep that in mind.Next you'll see the docstring, this is where you write a basic description of the function. Docstrings are not necessary for simple functions, but it's good practice to put them in so you or other people can easily understand the code you write.After all this you begin writing the code you wish to execute.The best way to learn functions is by going through examples. So let's try to go through examples that relate back to the various objects and data structures we learned about before.  Example 1: A simple print 'hello' function<jupyter_code>def say_hello():
    print('hello')<jupyter_output><empty_output><jupyter_text>Call the function:<jupyter_code>say_hello()<jupyter_output><empty_output><jupyter_text>Example 2: A simple greeting functionLet's write a function that greets people with their name.<jupyter_code>def greeting(name):
    print('Hello %s' %(name))
greeting('Bob')<jupyter_output><empty_output><jupyter_text>Using returnLet's see some example that use a return statement. return allows a function to *return* a result that can then be stored as a variable, or used in whatever manner a user wants. Example 3: Addition function<jupyter_code>def add_num(num1,num2):
    return num1+num2
add_num(4,5)
# Can also save as variable due to return
result = add_num(4,5)
print(result)<jupyter_output><empty_output><jupyter_text>What happens if we input two strings?<jupyter_code>add_num('one','two')<jupyter_output><empty_output><jupyter_text>Note that because we don't declare variable types in Python, this function could be used to add numbers or sequences together! We'll later learn about adding in checks to make sure a user puts in the correct arguments into a function.Let's also start using break, continue, and pass statements in our code. We introduced these during the while lecture. Finally let's go over a full example of creating a function to check if a number is prime (a common interview exercise).We know a number is prime if that number is only evenly divisible by 1 and itself. Let's write our first version of the function to check all the numbers from 1 to N and perform modulo checks.<jupyter_code>def is_prime(num):
    '''
    Naive method of checking for primes. 
    '''
    for n in range(2,num):
        if num % n == 0:
            print(num,'is not prime')
            break
    else: # If never mod zero, then prime
        print(num,'is prime!')
is_prime(16)
is_prime(17)<jupyter_output><empty_output><jupyter_text>Note how the else lines up under for and not if. This is because we want the for loop to exhaust all possibilities in the range before printing our number is prime.Also note how we break the code after the first print statement. As soon as we determine that a number is not prime we break out of the for loop.We can actually improve this function by only checking to the square root of the target number, and by disregarding all even numbers after checking for 2. We'll also switch to returning a boolean value to get an example of using return statements:<jupyter_code>import math

def is_prime2(num):
    '''
    Better method of checking for primes. 
    '''
    if num % 2 == 0 and num > 2: 
        return False
    for i in range(3, int(math.sqrt(num)) + 1, 2):
        if num % i == 0:
            return False
    return True
is_prime2(27)<jupyter_output><empty_output><jupyter_text>Why don't we have any break statements? It should be noted that as soon as a function *returns* something, it shuts down. A function can deliver multiple print statements, but it will only obey one return. Great! You should now have a basic understanding of creating your own functions to save yourself from repeatedly writing code!  Errors and Exception HandlingIn this section we will cover Errors and Exception Handling in Python. You've definitely already encountered errors by this point in the course. For example:<jupyter_code>print('Hello)<jupyter_output><empty_output><jupyter_text>Note how we get a SyntaxError, with the further description that it was an EOL (End of Line Error) while scanning the string literal. This is specific enough for us to see that we forgot a single quote at the end of the line. Understanding these various error types will help you debug your code much faster. This type of error and description is known as an Exception. Even if a statement or expression is syntactically correct, it may cause an error when an attempt is made to execute it. Errors detected during execution are called exceptions and are not unconditionally fatal.You can check out the full list of built-in exceptions [here](https://docs.python.org/3/library/exceptions.html). Now let's learn how to handle errors and exceptions in our own code.  try and exceptThe basic terminology and syntax used to handle errors in Python are the try and except statements. The code which can cause an exception to occur is put in the try block and the handling of the exception is then implemented in the except block of code. The syntax follows:    try:       You do your operations here...       ...    except ExceptionI:       If there is ExceptionI, then execute this block.    except ExceptionII:       If there is ExceptionII, then execute this block.       ...    else:       If there is no exception then execute this block. We can also just check for any exception with just using except: To get a better understanding of all this let's check out an example: We will look at some code that opens and writes a file:<jupyter_code>try:
    f = open('testfile','w')
    f.write('Test write this')
except IOError:
    # This will only check for an IOError exception and then execute this print statement
    print("Error: Could not find file or read data")
else:
    print("Content written successfully")
    f.close()<jupyter_output><empty_output><jupyter_text>Now let's see what would happen if we did not have write permission (opening only with 'r'):<jupyter_code>try:
    f = open('testfile','r')
    f.write('Test write this')
except IOError:
    # This will only check for an IOError exception and then execute this print statement
    print("Error: Could not find file or read data")
else:
    print("Content written successfully")
    f.close()<jupyter_output><empty_output><jupyter_text>Great! Notice how we only printed a statement! The code still ran and we were able to continue doing actions and running code blocks. This is extremely useful when you have to account for possible input errors in your code. You can be prepared for the error and keep running code, instead of your code just breaking as we saw above.We could have also just said except: if we weren't sure what exception would occur. For example:<jupyter_code>try:
    f = open('testfile','r')
    f.write('Test write this')
except:
    # This will check for any exception and then execute this print statement
    print("Error: Could not find file or read data")
else:
    print("Content written successfully")
    f.close()<jupyter_output><empty_output><jupyter_text>Great! Now we don't actually need to memorize that list of exception types! Now what if we kept wanting to run code after the exception occurred? This is where finally comes in. finallyThe finally: block of code will always be run regardless if there was an exception in the try code block. The syntax is:    try:       Code block here       ...       Due to any exception, this code may be skipped!    finally:       This code block would always be executed.For example:<jupyter_code>try:
    f = open("testfile", "w")
    f.write("Test write statement")
    f.close()
finally:
    print("Always execute finally code blocks")<jupyter_output><empty_output><jupyter_text>We can use this in conjunction with except. Let's see a new example that will take into account a user providing the wrong input:<jupyter_code>def askint():
    try:
        val = int(input("Please enter an integer: "))
    except:
        print("Looks like you did not enter an integer!")

    finally:
        print("Finally, I executed!")
    print(val)
askint()
askint()<jupyter_output><empty_output><jupyter_text>Notice how we got an error when trying to print val (because it was never properly assigned). Let's remedy this by asking the user and checking to make sure the input type is an integer:<jupyter_code>def askint():
    try:
        val = int(input("Please enter an integer: "))
    except:
        print("Looks like you did not enter an integer!")
        val = int(input("Try again-Please enter an integer: "))
    finally:
        print("Finally, I executed!")
    print(val)
askint()<jupyter_output><empty_output><jupyter_text>Hmmm...that only did one check. How can we continually keep checking? We can use a while loop!<jupyter_code>def askint():
    while True:
        try:
            val = int(input("Please enter an integer: "))
        except:
            print("Looks like you did not enter an integer!")
            continue
        else:
            print("Yep that's an integer!")
            break
        finally:
            print("Finally, I executed!")
        print(val)
askint()<jupyter_output><empty_output><jupyter_text>So why did our function print "Finally, I executed!" after each trial, yet it never printed `val` itself? This is because with a try-except-finally clause, any continue or break statements are reserved until *after* the try clause is completed. This means that even though a successful input of **3** brought us to the else: block, and a break statement was thrown, the try clause continued through to finally: before breaking out of the while loop. And since print(val) was outside the try clause, the break statement prevented it from running.Let's make one final adjustment:<jupyter_code>def askint():
    while True:
        try:
            val = int(input("Please enter an integer: "))
        except:
            print("Looks like you did not enter an integer!")
            continue
        else:
            print("Yep that's an integer!")
            print(val)
            break
        finally:
            print("Finally, I executed!")
askint()<jupyter_output><empty_output><jupyter_text>**Great! Now you know how to handle errors and exceptions in Python with the try, except, else, and finally notation!**  Modules and PackagesThe goal of this section is to:* code out a basic module and show how to import it into a Python script* run a Python script from a code block* show how command line arguments can be passed into a script Writing modules<jupyter_code>%%writefile myfile.py
def afunc(x):
    return [num for num in range(x) if num%2!=0]
mylist = afunc(20)<jupyter_output>Writing myfile.py<jupyter_text>**myfile.py** is going to be used as a module.Notice that **myfile.py** doesn't print or return anything,it just defines a function called *afunc* and a variable called *mylist*. Writing scripts<jupyter_code>%%writefile myfile2.py
import myfile
myfile.mylist.append(30)
print(myfile.mylist)<jupyter_output>Writing myfile2.py<jupyter_text>**myfile2.py** is a Python script.First, we import our **myfile** module (note the lack of a .py extension).Next, we access the *mylist* variable inside **myfile**, and perform a list method on it.`.append(30)` proves we're working with a Python list object, and not just a string.Finally, we tell our script to print the modified list. Running scripts<jupyter_code>! python myfile2.py<jupyter_output>[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 30]<jupyter_text>Here we run our script from the command line. The exclamation point is a Jupyter trick that lets you run command line statements from inside a jupyter cell.<jupyter_code>import myfile
print(myfile.mylist)<jupyter_output>[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]<jupyter_text>The above cell proves that we never altered **myfile.py**, we just appended a number to the list *after* it was brought into **myfile2**.  Passing command line argumentsPython's `sys` module gives you access to command line arguments when calling scripts.<jupyter_code>%%writefile myfile3.py
import sys
import myfile
num = int(sys.argv[1])
print(myfile.afunc(num))<jupyter_output>Overwriting myfile3.py<jupyter_text>Note that we selected the second item in the list of arguments with `sys.argv[1]`.This is because the list created with `sys.argv` always starts with the name of the file being used.<jupyter_code>! python myfile3.py 21<jupyter_output>[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]<jupyter_text>Here we're passing 21 to be the upper range value used by the *myfunc* function in **list1.py**  Understanding modulesModules in Python are simply Python files with the .py extension, which implement a set of functions. Modules are imported from other modules using the import command.To import a module, we use the import command. Check out the full list of built-in modules in the Python standard library [here](https://docs.python.org/3/py-modindex.html).The first time a module is loaded into a running Python script, it is initialized by executing the code in the module once. If another module in your code imports the same module again, it will not be loaded twice.If we want to import the math module, we simply import the name of the module:<jupyter_code># import the library
import math
# use it (ceiling rounding)
math.ceil(3.2)<jupyter_output><empty_output><jupyter_text>Exploring built-in modulesTwo very important functions come in handy when exploring modules in Python - the dir and help functions.We can look for which functions are implemented in each module by using the dir function:<jupyter_code>print(dir(math))<jupyter_output><empty_output><jupyter_text>When we find the function in the module we want to use, we can read about it more using the help function, inside the Python interpreter:<jupyter_code>help(math.ceil)<jupyter_output><empty_output><jupyter_text>Writing modulesWriting Python modules is very simple. To create a module of your own, simply create a new .py file with the module name, and then import it using the Python file name (without the .py extension) using the import command. Writing packagesPackages are name-spaces which contain multiple packages and modules themselves. They are simply directories, but with a twist.Each package in Python is a directory which MUST contain a special file called **\__init\__.py**. This file can be empty, and it indicates that the directory it contains is a Python package, so it can be imported the same way a module can be imported.If we create a directory called foo, which marks the package name, we can then create a module inside that package called bar. We also must not forget to add the **\__init\__.py** file inside the foo directory.To use the module bar, we can import it in two ways:<jupyter_code># Just an example, this won't work
import foo.bar
# OR could do it this way
from foo import bar<jupyter_output><empty_output><jupyter_text>In the first method, we must use the foo prefix whenever we access the module bar. In the second method, we don't, because we import the module to our module's name-space.The **\__init\__.py** file can also decide which modules the package exports as the API, while keeping other modules internal, by overriding the **\__all\__** variable, like so:<jupyter_code>__init__.py:

__all__ = ["bar"]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07060239549084077, 'fraction_numerical': 0.007926256458431188, 'mean_word_length': 4.823076923076923, 'pattern_counts': {'":': 0, '<': 279, '<?xml version=': 0, '>': 280, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '71', 'dir': 'jupyter-structured-clean-dedup', 'id': '193946', 'license': 'MIT', 'n_tokens_mistral': 9642, 'n_tokens_neox': 9291, 'n_words': 4664, 'path': 'Week2-Introduction-to-Python-_-NumPy/Intro to Python.ipynb', 'repo_name': 'atuljayaram/DigitalHistory', 'size': '34064'}