Module4 Organising Files and debugging
Module4 Organising Files and debugging
Prepared by: Dr. B Latha Shankar, Associate Professor, IEM Department, SIT Tumakuru
Syllabus: Organizing Files: The shutil Module, Walking a Directory Tree, Compressing Files with the zipfile Module,
Project: Renaming Files with American-Style Dates to European-Style Dates, Project: Backing Up a Folder into a ZIP
File
Debugging: Raising Exceptions, Getting the Traceback as a String, Assertions, Logging, IDLE’s Debugger.
Organizing Files
The shutil Module:
The shutil (or shell utilities) module helps in automating the process of copying, moving,
renaming and deleting files and directories in Python.
To use the shutil functions, import shutil.
shutil methods:
1. shutil.copy(source, destination) :
It will copy the file at the source path to the folder at the destination path .
Both source and destination can be strings /Path objects.
If destination is a filename, it will be used as the new name of the copied file.
This function returns, a string /Path object of the copied file
How shutil.copy() works?:
>>> import shutil, os #imports shutil and os mocules
>>> from pathlib import Path #imports pathlib module
>>> p = Path.home() # Assigns path object of home directory to variable p.
>>> shutil.copy(p / 'spam.txt', p / 'some_folder') #copies file ‘spam.txt’ at
#source to destination folder ‘some_folder’
O/P: 'C:\\Users\\Al\\some_folder\\spam.txt‘
# returns absolute path of new location of file ‘spam.txt’as a string
>>> shutil.copy(p / 'eggs.txt', p / 'some_folder/eggs2.txt')# ‘eggs2.txt’ is new
#name of copied file.
O/P: WindowsPath('C:/Users/Al/some_folder/eggs2.txt')
#returns absolute path of new location of file ‘eggs2.txt’as a string
2. shutil.copytree(source, destination):
It will copy folder at source path, along with all of its files and subfolders, to the folder at
the destination path.
The function returns a string of the path of the copied folder.
How shutil.copytree() works?:
>>> import shutil, os #imports shutil and os mocules
>>> from pathlib import Path #imports pathlib module
>>> p = Path.home() # Assigns path object of home directory to variable p.
>>> shutil.copytree(p / 'spam', p / 'spam_backup') #copies folder ‘spam’ at
#source to destination folder ‘spam_backup’
O/P: WindowsPath('C:/Users/Al/spam_backup)
#returns absolute path of new location of created folder spam_backup as a string
3. shutil.move(source, destination):
It will move the file or folder at the path source to the path destination
4-1
Module-4: Organizing Files and Debugging
4-2
Module-4: Organizing Files and Debugging
import os
for folderName, subfolders, filenames in os.walk('C:\\delicious'):
print('The current folder is ' + folderName)
for subfolder in subfolders:
print('SUBFOLDER OF ' + folderName + ': ' + subfolder)
for filename in filenames:
print('FILE INSIDE ' + folderName + ': '+ filename)
print(' ')
O/P:
The current folder is C:\delicious
SUBFOLDER OF C:\delicious: cats
SUBFOLDER OF C:\delicious: walnut
FILE INSIDE C:\delicious: spam.txt
4-3
Module-4: Organizing Files and Debugging
*3. ‘getinfo()’ object returns a ‘ZipInfo’ object about ‘ZipFile’ object. While a
‘ZipFile’ object represents an entire archive file, a ‘ZipInfo’ object holds useful information
about ‘ZipFile’ object. ZipInfo() object has attributes, such as file_size and
compress_size.
*4. Calculates how efficiently example.zip is compressed by dividing the original file size by
the compressed file size and prints this information.
>>> import zipfile, os Contents of
>>> from pathlib import Path example.zip
>>> p = Path.home() #Assign path of home folder to variable p
*1. >>> exampleZip = zipfile.ZipFile(p / 'example.zip')
*2. >>> exampleZip.namelist()
O/P:['spam.txt','cats/','cats/catnames.txt', 'cats/zophie.jpg']
*3. >>> spamInfo = exampleZip.getinfo('spam.txt')
>>> spamInfo.file_size # returns original file size in bytes
O/P: 13908
>>> spamInfo.compress_size # returns compressed file size in bytes
O/P: 3828
*4. >>> f'Compressed file is {round(spamInfo.file_size /
spamInfo.compress_size, 2)}x smaller!'
O/P:'Compressed file is 3.63x smaller!'
>>> exampleZip.close() # closes exampleZip
After running this code, the contents of ‘example.zip’ will be extracted to C:\, CWD.
Extract the files from example.zip into a newly created ‘C:\delicious’ folder, using:
>>> exampleZip.extractall('C:\\delicious')#extracts from exampleZIP file into
‘C:\delicious’ folder
4-4
Module-4: Organizing Files and Debugging
When you pass a path to the ‘write()’ method of a ZipFile object, Python will compress the file
at that path and add it into the ZIP file.
The ‘write()’ method’s first argument is a string of the filename to add.
The second argument is the compression type parameter, which tells the computer what
algorithm it should use to compress the files; always just set this value to
‘zipfile.ZIP_DEFLATED’
This code will create a new ZIP file named ‘new.zip’ that has compressed contents of ‘spam.txt’
Write mode will erase all existing contents of a ZIP file.
Pass 'a' as the second argument to ‘zipfile.ZipFile()’ to open the ZIP file in append mode.
Debugging
Raising Exceptions:
Even if a statement in Python is syntactically correct, it may cause an error when an attempt is
made to execute it. Errors detected during execution are called exceptions.
Python raises an exception whenever it tries to execute invalid code.
Python handles theses exceptions with try and except statements so that program can recover
from exceptions that were anticipated.
If there are no try and except statements covering the raise statement that raised the exception,
the program simply crashes and displays the exception’s error message.
A raise statement is seen inside a function and the try and except statements in the code calling
the function.
Ex: A boxPrint() function is defined that takes a character, a width, and a height, and uses the character
to make a little picture of a box with that width & height.
def boxPrint(symbol, width, height): # defines ‘boxprint’, receives 3 arguments
if len(symbol) != 1:
➊ raise Exception('Symbol must be a single character string.')
if width <= 2:
➋ raise Exception('Width must be greater than 2.')
if height <= 2:
➌ raise Exception('Height must be greater than 2.')
print(symbol * width)
for i in range(height - 2):
print(symbol + (' ' * (width - 2)) + symbol)
print(symbol * width)
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
try:
4-5
Module-4: Organizing Files and Debugging
Explanation:
This code uses a function named ‘boxPrint()’to print box shape on screen.
Conditions are the character should be a single character, and the width and height to be greater
than 2.
Statements are included to raise exceptions if these requirements aren’t satisfied.
When boxPrint() is called with various arguments, try/except will handle invalid arguments.
If an Exception object is returned from boxPrint() ➊ ➋ ➌, except statement will store it in a
variable named err.
We can then convert the Exception object to a string by passing it to str() to produce a
userfriendly error message ➍.
Output:
****
* *
* *
****
OOOOOOOOOOOOOOOOOOOO
O O
O O
O O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.
Output:
Traceback (most recent call last):
File "errorExample.py", line 7, in <module> spam()
File "errorExample.py", line 2, in spam bacon()
File "errorExample.py", line 5, in bacon
raise Exception('This is the error message.')
Exception: This is the error message.
From traceback, one can see that the error happened on line 5, in the bacon() function.
This particular call to bacon() came from line 2, in the spam() function, which in turn was called
on line 7.
4-6
Module-4: Organizing Files and Debugging
In programs where functions can be called from multiple places, call stack can help to determine
which call led to the error.
Python displays the traceback exception as a string by calling traceback.format_exc().
Need to import Python’s traceback module for using this function.
For example, instead of crashing the program right when an exception occurs, write the
traceback information to a text file and keep your program running.
You can look at the text file later, when you’re ready to debug your program.
Ex:
>>> import traceback
>>> try:
... raise Exception('This is the error message.')
except:
... errorFile = open('errorInfo.txt', 'w')
... errorFile.write(traceback.format_exc())
... errorFile.close()
... print('The traceback info was written to errorInfo.txt.')
Output:
111 # write() method returns 111, since 111 aracters were written to file.
The traceback info was written to errorInfo.txt.
Assertions:
An assertion is a sanity check to make sure code isn’t doing something obviously wrong.
If the sanity check fails, then an AssertionError exception is raised.
An assert statement consists of the following:
The assert keyword
A condition ( an expression that evaluates to True or False)
A comma
A string to display when the condition is False
An assert statement says, “I assert that the condition holds true, and if not, there is a bug
somewhere, so immediately stop the program.”
Ex:
>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.sort()
>>> ages
O/P: [15, 17, 22, 26, 47, 54, 57, 73, 80, 92]
>>> assert ages[0] <= ages[-1] # Assert that the first age is <= the last age.
The assert statement here asserts that the first item in ages should be less than or equal to the
last one.
Because the ages[0] <= ages[-1] expression evaluates to True, the assert statement does nothing.
Suppose if bug is present in code - ex:
Say we accidentally called the reverse() method instead of the sort() method.
Then assert statement raises an AssertionError:
>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.reverse()
4-7
Module-4: Organizing Files and Debugging
>>> ages
O/P: [73, 47, 80, 17, 15, 22, 54, 92, 57, 26]
>>> assert ages[0] <= ages[-1] # Assert that the first age is <= the last age.
O/P: Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Assertions – Advantages:By “failing fast” using Assertion statement, the time between the
original cause of the bug and when you first notice the bug gets shortened. This will reduce
amount of code you will have to check before finding bug’s cause.
Code should not handle assert statements with try and except; if an assert fails, program should
crash.
Assertions are for programmer errors, not user errors.
Assertions should only fail while the program is under development; a user should never see an
assertion error in a finished program.
For errors that program identifies as a normal part of its operation (Ex: a file not found or the
user entering invalid data), raise an exception instead of detecting it with an assert statement.
You shouldn’t use assert statements in place of raising exceptions, because users can choose to
turn off assertions.
Assertions also aren’t a replacement for comprehensive testing:
Ex.:if the previous ages was set to [10, 3, 2, 1, 20]:
assert ages[0] <= ages[-1]
The list was unsorted, but the assertion wouldn’t check it.
Further, rest of the simulation code is written, thousands of lines long, without noticing a bug.
When you finally run the simulation, the program doesn’t crash—but your virtual cars do!
Since you’ve already written the rest of the program, you have no idea where the bug could be.
4-8
Module-4: Organizing Files and Debugging
It could take hours to trace the bug back to the switchLights() function.
If you had added an assertion to check that at least one of the lights is always red, one can save a
lot of future debugging effort :
assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight)
With this assertion in place, program would crash with this error message:
Traceback (most recent call last):
File "carSim.py", line 14, in <module>
switchLights(market_2nd)
File "carSim.py", line 13, in switchLights
assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight)
➊ AssertionError: Neither light is red! {'ns': 'yellow', 'ew': 'green'}
The program immediately points out that a sanity check failed, printing AssertionError ➊.
Neither direction of traffic has a red light, meaning that traffic could be going both ways.
By failing fast early in the program’s execution, you can save yourself a lot of future debugging
effort.
Logging
Logging is a way to track events that occur. Essential for debugging a program while developing.
It is a great way to understand what’s happening in program and in what order it’s happening.
Without logging, finding the source of a problem in code may be extremely time consuming.
Using a print() statement in code to output some variable’s value while program is running, is a
form of logging to debug code.
Python’s logging module will describe when the program execution has reached the logging
function call and list any variables are specified at that point in time.
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s -
%(message)s')
When Python logs an event, it creates a LogRecord object that holds information about that
event.
The logging module’s basicConfig() function lets to specify what details about LogRecord
object user wants to see and how he wants those details displayed.
print(factorial(5))
logging.debug('End of program')
Output:
2019-05-23 16:20:12,664 - DEBUG - Start of program
2019-05-23 16:20:12,664 - DEBUG - Start of factorial(5)
2019-05-23 16:20:12,665 - DEBUG - i is 0, total is 0
2019-05-23 16:20:12,668 - DEBUG - i is 1, total is 0
2019-05-23 16:20:12,670 - DEBUG - i is 2, total is 0
2019-05-23 16:20:12,673 - DEBUG - i is 3, total is 0
2019-05-23 16:20:12,675 - DEBUG - i is 4, total is 0
2019-05-23 16:20:12,678 - DEBUG - i is 5, total is 0
2019-05-23 16:20:12,680 - DEBUG - End of factorial(5)
0
2019-05-23 16:20:12,684 - DEBUG - End of program
Output:
2019-05-23 17:13:40,650 - DEBUG - Start of program
2019-05-23 17:13:40,651 - DEBUG - Start of factorial(5)
2019-05-23 17:13:40,651 - DEBUG - i is 1, total is 1
2019-05-23 17:13:40,654 - DEBUG - i is 2, total is 2
2019-05-23 17:13:40,656 - DEBUG - i is 3, total is 6
2019-05-23 17:13:40,659 - DEBUG - i is 4, total is 24
2019-05-23 17:13:40,661 - DEBUG - i is 5, total is 120
2019-05-23 17:13:40,661 - DEBUG - End of factorial(5)
120
2019-05-23 17:13:40,666 - DEBUG - End of program
For messages that the user will want to see, (Ex: File not found or Invalid input, please enter a
number), use a print() call.
Logging Levels:
Logging levels categorize log messages by importance. There are five logging levels, least to most
important:
Logging Levels in Python
Level Logging function Description
logging.debug()
DEBUG The lowest level. Used for testing
logging.info()
INFO The information level, to confirm that things are
working at their point in the program.
logging.warning()
WARNING The warning level, to indicate something could
go wrong.
logging.error()
ERROR The error level, to indicate something has gone
wrong.
logging.critical()
CRITICAL The highest level, where something has gone
wrong and might stop the program.
Your logging message is passed as a string to these logging functions:
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s-%(levelname)s-
%(message)s')
>>> logging.debug('Some debugging details.')
2019-05-18 19:04:26,901 - DEBUG - Some debugging details.
>>> logging.info('The logging module is working.')
2019-05-18 19:04:35,569 - INFO - The logging module is working.
>>> logging.warning('An error message is about to be logged.')
2019-05-18 19:04:56,843 - WARNING - An error message is about to be logged.
>>> logging.error('An error has occurred.')
2019-05-18 19:05:07,737 - ERROR - An error has occurred.
>>> logging.critical('The program is unable to recover!')
2019-05-18 19:05:45,794 - CRITICAL - The program is unable to recover!
Benefit of logging levels is that you can change what priority of logging message you want to see.
Passing logging.DEBUG to basicConfig() function’s level keyword argument will show messages
from all the logging levels (DEBUG being lowest level)
If you are interested only in errors, set basicConfig()’s level argument to logging.ERROR.
This will show only ERROR and CRITICAL messages and skip the DEBUG, INFO and WARNING
messages.
4-11
Module-4: Organizing Files and Debugging
After you’ve debugged your program, you don’t want all these log messages cluttering the screen.
Use logging.disable() to disable all messages after it /near the ‘import logging’ line in program.
This way, you can comment out or uncomment that call to enable or disable logging messages as
needed.
Logging to a File:
Instead of displaying the log messages on to the screen, they can be written to a text file.
The ‘logging.basicConfig()’ function takes a filename keyword argument:
The log messages will be saved to myProgramLog.txt.
Advantage: While logging messages are helpful, they clutter the screen and make it hard to read
the program’s output.
Writing the logging messages to a file will keep the screen clear and store the messages so that
you can read them in any text editor, after running the program.
import logging
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='
%(asctime)s - %(levelname)s - %(message)s')
Mu’s Debugger
The debugger is a a valuable tool for tracking down bugs.
It is a feature of the Mu editor, or any other editor software that allows to execute program one
line at a time and then wait for you to tell it to continue.
By running your program “under the debugger” like this, you can take as much time as you want
to examine values in variables at any given point during the program’s lifetime.
To run a program under Mu’s debugger, click the Debug button in the top row of buttons, next to
the Run button.
Along with the usual output pane at the bottom, the Debug Inspector pane will open along the
right side of the window.
This pane lists the current value of variables in your program.
Debugging mode also adds the following new buttons to the top of the editor: Continue, Step
Over, Step In, and Step Out. The usual Stop button is also available.
Continue: Continue button will cause the program to execute normally until it terminates or
reaches a breakpoint. If you are done debugging and want the program to continue normally,
click the Continue button.
Step In : Step In button will cause the debugger to execute the next line of code and then pause
again. If the next line of code is a function call, the debugger will “step into” that function and
jump to the first line of code of that function.
Step Over: This button will execute the next line of code, similar to Step In button. If the next line
of code is a function call, the Step Over button will “step over” the code in the function. The
function’s code will be executed at full speed, and the debugger will pause as soon as the function
call returns. Ex: if the next line of code calls a spam() function but you don’t really care about code
inside this function, you can click Step Over to execute the code in the function at normal speed,
and then pause when the function returns.
Step Out: This button will cause the debugger to execute lines of code at full speed until it returns
from the current function. If you have stepped into a function call with the Step In button and now
simply want to keep executing instructions until you get back out, click the Out button to “step
out” of the current function call.
4-12
Module-4: Organizing Files and Debugging
Stop: If you want to stop debugging entirely and not bother to continue executing the rest of the
program, click this button. The Stop button will immediately terminate the program.
4-13
Module-4: Organizing Files and Debugging
➊ if random.randint(0, 1) == 1:
heads = heads + 1
if i == 500:
➋ print('Halfway done!')
print('Heads came up ' + str(heads) + ' times.')
The random.randint(0, 1) call ➊ will return 0 half of the time and 1 the other half of the time.
This can be used to simulate a 50/50 coin flip where 1 represents heads.
When you run this program without the debugger, it quickly outputs: Halfway done!
Heads came up 490 times.
If you ran this program under the debugger, you would have to click the Step Over button
thousands of times before the program terminated.
If you were interested in the value of heads at the halfway point of the program’s execution,
when 500 of 1,000 coin flips have been completed, then set a breakpoint on the line
print('Halfway done!') ➋.
To set a breakpoint, click the line number in the file editor to cause a red dot to appear.
Don’t set a breakpoint on the if statement line, since if statement is executed on every single
iteration through loop.
The line with the breakpoint will have a red dot next to it.
When you run the program under the debugger, it will start in a paused state at the first line, as
usual.
But if you click Continue, the program will run at full speed until it reaches the line with the
breakpoint set on it.
Then click Continue, Step Over, Step In, or Step Out to continue as normal.
If you want to remove a breakpoint, click the line number again.
The red dot will go away, and debugger will not break on that line in future.
at path, which is
empty of any files or
folders.
6. shutil.rmtree(path) shutil.move('C:\\some_folder\\
will remove the folder bacon.txt')
at path, and all files
and folders it
contains will also be
deleted.
Debugging:
Raising Exceptions:
17. raise Exception(‘Message') Raise an user-defined raise Exception('Symbol must be a
single character string.')
exception
18. traceback.format_exc() displays the traceback errorFile.write(traceback.format_exc())
exception as a string
19. variablename = displays the traceback
errorFile = open('errorInfo.txt', 'w')
open(filename, 'w') exception as a string errorFile.write(traceback.format_exc())
by calling
variablename.write(traceba traceback.format_exc
ck.format_exc())
() and write it to a text
file, 'errorInfo.txt'
Assertions
20. assert condition, message Condition if True does assert 'red' in stoplight.values(),
'Neither light is red! '
nothing and if False
raises an
AssertionError
Logging
21. The logging module’s logging.debug()will call logging.basicConfig(level=logging.DE
logging.basicConfig() basicConfig(), and a line BUG, format=' %(asctime)s -
%(levelname)s - %(message)s')
of information in the
format as specified in
basicConfig() will be
printed and will include
the messages that are
passed to debug().
Used to rename thousands of files with American-style dates (MM-DD-YYYY) in their names to
European-style dates (DD-MM-YYYY).
Steps involved:
It searches all the filenames in the cwd for American-style dates.
When one is found, it renames file with month & day swapped to make it European-style.
o 1. Creates a regex that can identify the text pattern of American-style dates.
o 2. Call os.listdir() to find all the files in the working directory.
o 3. Loop over each filename, using the regex to check whether it has a date.
o 4. If it has a date, rename the file with shutil.move().
4-16
Module-4: Organizing Files and Debugging
Step 2: # Code to find all files in CWD and loop over them:
for amerFilename in os.listdir('.'):
mo = datePattern.search(amerFilename)
# Skip files without a date.
➊if mo == None:
➋ continue
# ➌ Get the different parts of the filename.
beforePart = mo.group(1)
monthPart = mo.group(2)
dayPart = mo.group(4)
yearPart = mo.group(6)
afterPart = mo.group(8)
--snip—
Explanation:
Next, the program will have to loop over the list of filename strings returned from os.listdir() and
match them against the regex.
Any files that do not have a date in them should be skipped.
For filenames that have a date, the matched text will be stored in several variables.
If the Match object returned from the search() method is None ➊, then the filename in
amerFilename does not match the regular expression.
Continue statement ➋ will skip rest of loop and move on to next filename.
Otherwise, the various strings matched in the regular expression groups are stored in variables
named beforePart, monthPart, dayPart, yearPart and afterPart ➌.
The strings in these variables will be used to form the European-style filename in the next step
To get more understanding about the groups; count up each time you encounter an opening
parenthesis.
Ex:
datePattern = re.compile(r"""^(1) # all text before the date
4-17
Module-4: Organizing Files and Debugging
Explanation:
As the final step, concatenate the strings in the variables made in the previous step with the
European-style date: date comes before month.
Store the concatenated string in a variable named euroFilename ➊.
Then, pass the original filename ‘amerFilename’ and the new ‘euroFilename’ variable to the
‘shutil.move()’ function to rename the file ➌.
Before running this program, comment out ‘shutil.move()’ call and run it so that it only prints the
filenames that are to be renamed ➋.
This ensures you to double-check that the files will be renamed correctly.
Then you can uncomment the ‘shutil.move()’ and run the program again to actually rename the
files.
If this double checking is not done, you may accidentally rename the files that are not to be
renamed.
mo = datePattern.search(amerFilename)
Explanation:
The code for this program will be placed into a function named backupToZip()
The first part, naming the ZIP file, uses the base name of the absolute path of folder. Ex: If the
folder being backed up is C:\delicious (delicious is basename here), the ZIP file’s name should be
delicious_N.zip, where N = 1 is the first time you run the program, N = 2 is the second time, and
so on.
4-19
Module-4: Organizing Files and Debugging
You can determine what N should be by checking whether delicious_1.zip already exists, then
checking whether delicious_2.zip already exists, and so on.
Use a variable named ‘number’ for ‘N’ ➋, and keep incrementing it inside the loop that calls
os.path.exists() to check whether the file exists ➌.
The first nonexistent filename found will cause the loop to break, since it will have found the
filename of the new zip.
Create the ZIP file, using ‘zipfile.ZipFile()’ to actually create ZIP file ➊.
Store the new ZIP file’s name in the zipFilename variable.
Pass 'w' as second argument so that the ZIP file is opened in write mode.
Step 3: Walk the Directory Tree and Add to the ZIP File
--snip--
# Walk the entire folder tree and compress the files in each folder.
➊ for foldername, subfolders, filenames in os.walk(folder):
print(f'Adding files in {foldername}...')
# Add the current folder to the ZIP file.
➋ backupZip.write(foldername)
# Add all the files in this folder to the ZIP file.
➌ for filename in filenames:
newBase = os.path.basename(folder) + '_'
if filename.startswith(newBase) and filename.endswith('.zip'):
continue # don't back up the backup ZIP files
backupZip.write(os.path.join(foldername, filename))
backupZip.close()
print('Done.')
backupToZip('C:\\delicious')
Explanation:
Now you need to use the ‘os.walk()’ function to do the work of listing every file in the folder
and its subfolders.
You can use os.walk() in a for loop ➊, and on each iteration it will return the iteration’s current
folder name, the subfolders in that folder, & filenames in that folder.
In the for loop, the folder is added to the ZIP file ➋.
The nested for loop can go through each filename in the ‘filenames’ list ➌.
Each of these is added to the ZIP file, except for previously made backup ZIPs.
When you run this program, it will produce output that will look like this:
Output:
4-20
Module-4: Organizing Files and Debugging
Creating delicious_1.zip...
Adding files in C:\delicious...
Adding files in C:\delicious\cats...
Adding files in C:\delicious\waffles...
Adding files in C:\delicious\walnut...
Adding files in C:\delicious\walnut\waffles...
Done.
Second time you run it, it will put all the files in C:\delicious into a ZIP file named delicious_2.zip,
and so on.
4-21