Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Events
Videos
Audiobooks
Packt Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Python Automation Cookbook
Python Automation Cookbook

Python Automation Cookbook: 100+ new and updated recipes for scalable workflows, MCP integrations, and AI-powered automation , Third Edition

Arrow left icon
Profile Icon Jaime Buelta
Arrow right icon
$32.39 $35.99
eBook Jun 2026 676 pages 3rd Edition
eBook
$32.39 $35.99
Paperback
$44.99
eBook + Subscription
$29.99 Monthly
Arrow left icon
Profile Icon Jaime Buelta
Arrow right icon
$32.39 $35.99
eBook Jun 2026 676 pages 3rd Edition
eBook
$32.39 $35.99
Paperback
$44.99
eBook + Subscription
$29.99 Monthly
eBook
$32.39 $35.99
Paperback
$44.99
eBook + Subscription
$29.99 Monthly

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Table of content icon View table of contents Preview book icon Preview Book

Python Automation Cookbook

1

Let's Begin Our Automation Journey

The objective of this chapter is to lay down some of the basic techniques that will be useful throughout the whole book. The main idea is to create a good Python environment in which to run the automation tasks that follow and be able to parse text inputs into structured data.

We will introduce generative artificial intelligence (GenAI) tools. Throughout the book we will looking at how they can help us in our automation projects. In this chapter we will get a taste of that, and consider some of their limitations.

Python has a good number of tools installed by default, but it also makes it easy to install third-party tools that can simplify many common operations. We'll see how to import modules from external sources and use them to leverage the full potential of Python.

We will install and use tools to help process texts. The ability to structure input data is critical in any automation task. Most of the data that we will process in this book will come from unformatted sources such as web pages or text files. As the old computer adage says, garbage in, garbage out, making the sanitizing of inputs a very important task.

In this chapter, we'll cover the following topics:

  • The elephant in the room: vibe coding
  • Working with multiple Python versions and virtual environments through uv
  • Installing third-party packages
  • Creating strings with formatted values
  • Manipulating strings
  • Extracting data from structured strings
  • Using a third-party tool—parse
  • Introducing regular expressions
  • Going deeper into regular expressions
  • Adding command-line arguments

Your purchase includes a free PDF copy + exclusive extras

Your purchase includes a DRM-free PDF copy of this book, 7-day trial to the Packt+ library (no credit card required), and additional exclusive extras. See the Free benefits with your book section in the Preface to unlock them instantly and maximize your learning.

Technical requirements

Python requirements are as set out in the Preface, and we shall also be using the uv tool, installation of which is described in the next section but one.

The code presented in this chapter can be found in the book's GitHub repository, at https://github.com/PacktPublishing/Python-Automation-Cookbook-Third-Edition/tree/main/ch01.

The elephant in the room: vibe coding

Since the introduction of ChatGPT and other related tools, the coding world has been living an ongoing revolution where we can generate code with the help of generative artificial intelligence (GenAI). These tools, based on what are called large language models (LLMs), allow users to describe in plain language a piece of required code, and the computer then returns the code itself, along with some comments and explanation.

For the majority of the book, we will predominantly be using ChatGPT, as it's the most commonly available tool. You can create a free account at https://chatgpt.com/.

Other alternatives include Anthropic's Claude (https://claude.ai/), Google's Gemini (https://gemini.google.com/) or Microsoft's Copilot (https://copilot.microsoft.com).

These instructions that we give the GenAI tool are known as prompts. For example, to perform a very simple operation in Python:

Figure 1.1: ChatGPT conversation

Figure 1.1: ChatGPT conversation

If we copy the code, save it to a file named simple_script.py and execute it, we see the expected result:

$ python3 simple_script.py
First number: 60
Second number: 81
Sum: 141

While this is a very simple example, you can complicate the instructions and keep iterating on the result to create quite complex pieces of code, with only a minimal amount of coding knowledge. The fact that with GenAI you have your own expert coder that you can ask, in your own words, to generate code at a fast pace is truly revolutionary, and it's empowering a great many people to upscale their coding capacities.

At the same time, it's not without its risks and limitations:

  • You have a pretty capable artificial developer at your disposal, but, at the same time, it can be very obtuse: it tends to understand what you say, but not what you really want. Any good developer will push back and ask insightful clarifying questions to understand the real problem to fix, and not just blindly do what they are asked to do.
  • It works well for small, well-defined problems, but the amount of data that a particular LLM can deal with at the same time is limited. This is known as context. When the context is full, the LLM is way less effective. While current tools are getting better at automatically handling this context behind the scenes, you may have experienced issues in long conversations with ChatGPT, for example, where the longer a conversation runs, the weirder it gets. This creates problems as codebases grow bigger, and requires methods capable of working with that scale.
  • They can generate code at such a fast pace that it exceeds the capacity to absorb and understand it. When you are doing your own coding, you develop an intimate knowledge of the code, and that's not present when you delegate the writing of code. Fully delegating to an AI tool means that, ultimately, no one has that deep knowledge of the code produced. That can create a liability and make the code very difficult to understand and maintain, making it prone to bugs or unexpected behaviors.
  • There are also risks related to the security of the code generated. LLMs can be tricked into producing bad effects, making it very difficult to verify that the code will be safe to use.

But tools are getting better and better, and LLMs are also improving all the time. That has led to a process that has been called vibe coding. The term itself was initially described by Andrej Karpathy in a tweet:

There's a new kind of coding I call "vibe coding", where you fully give in to the vibes, embrace exponentials, and forget that the code even exists. It's possible because the LLMs … are getting too good. …When I get error messages I just copy paste them in with no comment, usually that fixes it. The code grows beyond my usual comprehension, I'd have to really read through it for a while. Sometimes the LLMs can't fix a bug so I just work around it or ask for random changes until it goes away. It's not too bad for throwaway weekend projects, but still quite amusing. I'm building a project or webapp, but it's not really coding - I just see stuff, say stuff, run stuff, and copy paste stuff, and it mostly works.

While the term can be interpreted in different ways, it normally understood to mean working with code without actually coding, instead iterating through a tool to perform code changes, not caring unduly about the resulting code, but only about its results.

While this is not suitable for all coding work, it can be effective for small, controlled projects. Which may be the case for many of the recipes that we describe in this book! Just be wary about delegating everything to the tool.

As important as generating code, GenAI is also a fantastic tool to explain code. You can learn a lot by using it to explain what is going on in some piece of code that you don't understand completely.

After that brief introduction to the new vistas in software development opened by the advent of GenAI, we now turn to a slightly more mundane, but no less important, topic: using virtual environments to prepare our project to run the proper Python version and libraries.

Working with multiple Python versions and virtual environments through uv

As a first step when working with Python, it is a good practice to explicitly define the working environment.

This helps you to detach from the operating system interpreter and environment and properly define the dependencies that will be used. Not doing so tends to generate chaotic scenarios. Remember, explicit is better than implicit!

Explicit is better than implicit is one of the most quoted parts of the Zen of Python. The Zen of Python is a list of general guidelines for Python programming, intended to provide clarity on what is considered Pythonic. The full Zen of Python can be invoked from the Python interpreter by calling import this.

This explicitness is especially important in two scenarios:

  • When dealing with multiple projects on the same computer, as they can have different dependencies that may clash at some point. For example, two versions of the same module cannot be installed in the same environment.
  • When working on a project that will be used on a different computer, for example when developing some code on a personal laptop that will ultimately run in a remote server.

A common joke among developers is responding to a bug with 'it runs on my machine', meaning that it appears to work on their laptop but not on the production servers. Although a huge number of factors can produce this error, a good practice is to produce an automatically replicable environment, reducing uncertainty over what dependencies are really being used.

In Python 3 the venv module, which sets up a local virtual environment, is included as part of the standard library. (This was not the case in the previous version, where you had to install the external virtualenv package.) Using the venv module, none of the installed dependencies will be shared with the Python interpreter installed on the machine, creating an isolated environment.

In recent years a new tool has simplified the use of environments, installation of third-party modules and running them: uv. This tool is quickly being adopted by the Python community as it simplifies the work and is much quicker than the previous alternatives!

The command pip, included in Python, is used to install dependencies. When using it with an active virtual environment, it installs the required packages in it. uv simplifies the operation by managing automatically what virtual environment to use, based on the working directory, and where to install the packages.

Getting ready

Install uv if not already installed. The easiest way is to install it through the standard pip command:

$ pip install uv
Collecting uv
…
Installing collected packages: uv
Successfully installed uv-0.9.9

Before creating a virtual environment, check what Python binary you are calling:

$ uv run python -c "import sys; print(sys.executable)"
/usr/local/opt/python@3.14/bin/python3.14

How to do it…

You can create your local environment by calling uv venv:

$ uv venv
Using CPython 3.14.0 interpreter at: /usr/local/opt/python@3.14/bin/python3.14
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

You can see that now calling uv run returns the Python interpreter in your virtual environment instead of the interpreter in your system:

$ uv run python -c "import sys; print(sys.executable)"
/your/local/path/.venv/bin/python3

So far, this is a wrapper to create a virtualenv, but note that if we use uv we don't need to activate the virtual environment, we can just call it through uv run and it will run inside the environment:

$ uv run
Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.

The following commands are available in the environment:

- python
- python3
- python3.14

See `uv run --help` for more information.

When running scripts inside uv run, it automatically uses any virtual environment in the directory. Previously you needed to manually enter the virtual environment and leave it.

It's also more explicit with the version of Python that you are using, and you can use a different version if necessary.

To force the version of Python to use, you can use the uv pin command:

$ uv python pin 3.14
Pinned `.python-version` to `3.14`
$ uv python pin
3.14
$ uv run python
Python 3.14.0 (main, Oct  7 2025, 09:34:52) [Clang 17.0.0 (clang-1700.3.19.1)] on Darwin
>>> exit()
$ "license" for more information.

If you want to use a different version of Python, uv will download it if necessary:

$ uv python pin 3.15.0a1
Updated `.python-version` from `3.14` -> `3.15a1`
$ uv run python
Python 3.15.0a1 (main, Oct 31 2025, 23:15:40) [Clang 21.1.4 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()

The specific version is stored in a file called .python‑version

Be careful with discrepancies between the virtual environment and the Python version! If the Python version doesn't match the version on the virtual environment, it will use the pinned version. You may need to recreate the virtual environment by calling uv venv again.

To be sure that we use the Python 3.14 version, the last one fully released at the time of writing, pin it back again:

$ uv python pin 3.14
Pinned `.python-version` to `3.14`
Jaimes-iMac-24:chapter01 jaime$ uv run python -c "import sys; print(sys.executable)"
/your/local/path/.venv/bin/python3

How it works

uv simplifies the workflow by using the defined virtual environment. The virtual environment can contain its own version, third-party packages, etc.

The virtual environment contains all the Python data in the .venv directory. The best thing about it is that it can be deleted and recreated very easily, removing the fear of experimenting in a self-contained sandbox.

By default uv will use the .venv name for the subdirectory, but this can be overwritten by using the –directory argument.

Once created, the .venv works as a regular virtual environment in Python as well, and you can activate it manually, which will show it active by default with the name of the directory:

$ pwd
/your/local/path/.
$ source .venv/bin/activate
(path) $ which python
/your/local/path/.venv/bin/python3
(path) $ deactivate
$ which python3
/usr/local/bin/python3

This name can be changed with use of –prompt when creating the virtual environment.

There's more

uv is a pretty powerful tool and we have barely scratched the surface. The most important use case throughout the book will be to install third-party packages, as we will see in the next recipe, but take a look at the full documentation at https://docs.astral.sh/uv/ for others.

uv has an important use case for defining complex projects and handling dependencies, build packages, etc. While this is pretty powerful, it can only be confusing for the automation projects that are the focus of this book as it can be, frankly, a bit too much of an overhead. My recommendation is to use uv as a tool for creating virtual environments, installing packages and running scripts, unless you have a clear power use around packaging.

See also

  • The Installing third-party packages recipe, coming next.
  • The Using a third-party tool—parse recipe, later in the chapter.

Installing third-party packages

One of the strongest capabilities of Python is the ability to use an impressive catalog of third-party packages that cover an amazing amount of ground in different areas, from modules specialized in performing numerical operations, machine learning, and network communications, to command-line convenience tools, database access, image processing, and much more!

Most of them are available on the official Python Package Index (https://pypi.org/), which has more than 200,000 packages ready to use. In this book, we'll install some of them. In general, it's worth spending a little time researching external tools when trying to solve a problem. It's very likely that someone else has already created a tool that solves all, or at least part, of the problem.

More important than finding and installing a package is keeping track of which packages are being used. This greatly helps with replicability, meaning the ability to start the whole environment from scratch in any situation.

Getting ready

The starting point is to find a package that will be of use in our project.

A great one is requests, a module that deals with HTTP requests and is known for its easy and intuitive interface, as well as its great documentation. Take a look at the documentation, which can be found here:https://requests.readthedocs.io/en/latest/.

We'll use requests throughout this book when dealing with HTTP connections.

The next step will be to choose the version to use. In this case, the latest (2.32.5, at the time of writing) will be perfect. If the version of the module is not specified, by default it will install the latest version, which can lead to inconsistencies in different environments as newer versions are released.

We'll also use the great pendulum module for time handling (version 3.2.0: https://pendulum.eustace.io).

How to do it…

Create a requirements.txt file in our main directory, which will specify all the requirements for our project. Let's start with pendulum and requests:

pendulum==3.2.0
requests==2.32.5

Install all the requirements with the uv pip command:

$ uv pip install -r requirements.txt
Resolved 10 packages in 385ms
Installed 10 packages in 55ms
 + certifi==2026.5.20
 + charset-normalizer==3.4.7
 + idna==3.18
 + parse==1.20.2
 + pendulum==3.2.0
 + python-dateutil==2.9.0.post0
 + requests==2.32.5
 + six==1.17.0
 + tzdata==2026.2
 + urllib3==2.7.0

If you have previously used pip to install packages, you may be finding out that using uv is much faster.

Show the available modules installed using uv pip list:

$ uv pip list
Package            Version
------------------ -----------
certifi            2026.5.20
charset-normalizer 3.4.7
idna               3.18
parse              1.20.2
pendulum           3.2.0
python-dateutil    2.9.0.post0
requests           2.32.5
six                1.17.0
tzdata             2026.2
urllib3            2.7.0

You can now use both modules when using the virtual environment:

$ uv run python
Python 3.14.0 (main, Oct  7 2025, 09:34:52) [Clang 17.0.0 (clang-1700.3.19.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pendulum
>>> import requests
>>>

How it works…

The requirements.txt file specifies the module and version, and uv pip performs a search on pypi.org.

Note that creating a new virtual environment from scratch and running the following will completely recreate your environment, which makes replicability very straightforward:

$ uv pip install -r requirements.txt

Note that step 2 of the How to do it… section automatically installs other modules that are dependencies, such as urllib3.

There's more…

If any of the modules need to be changed to a different version because a new version is available, change them using requirements and run the install command again:

$ uv pip install -r requirements.txt

This is also applicable when a new module needs to be included.

At any point, the pipfreeze command can be used to display all of the installed modules. freeze returns the modules in a format compatible with requirements.txt, making it possible to generate a file with our current environment:

$ uv pip freeze > requirements.txt

This will include dependencies, so expect a lot more modules in the file.

Finding great third-party modules is sometimes not easy. Searching for specific functionality can work well, but sometimes there are great modules that are a surprise because they do things you never thought of. A great curated list is Awesome Python (https://github.com/vinta/awesome-python), which covers a lot of great tools for common Python use cases, such as cryptography, database access, date and time handling, and more.

GenAI can help with finding a suitable module for a particular use case.

Generally, it will be knowledgeable and direct you towards using common packages, such as requests. But you can ask about specific use cases and recommendations. Don't take a GenAI recommendation at face value, however: check the project itself and verify that it's what you need. A common problem from GenAI is that it will use an stablished older package that perhaps is not the best for your specific usage, for example, because there's a newer package that will get you better results. GenAI can help with the search, but not take the decision for you.

In some cases, installing packages may require additional tools, such as compilers or a specific library that supports specific functionality (for example, a particular database driver). If that's the case, the documentation will explain the dependencies.

See also

  • The Working with multiple Python versions and virtual environments with uv recipe, earlier in this chapter.
  • The Using a third-party tool—parse recipe, later in this chapter, to learn how to use one installed third-party module.

Creating strings with formatted values

One of the basic abilities when creating text and documents is to be able to properly format values into structured strings. Python is smart at presenting good defaults, such as properly rendering a number, but there are a lot of options and possibilities.

We'll discuss some of the common options when creating formatted text using the example of a table.

Getting ready

The main tool to format strings in Python is the format method. It works with a defined mini-language to render variables this way:

result = template.format(*parameters)

The template is a string that is interpreted based on the mini-language. At its simplest, templating replaces the values between the curly brackets with the parameters. Here are a couple of examples:

>>> 'Put the value of the string here: {}'.format('STRING')
"Put the value of the string here: STRING"
>>> 'It can be any type ({}) and more than one ({})'.format(1.23, 'STRING')
"It can be any type (1.23) and more than one (STRING)"
>>> 'Specify the order: {1}, {0}'.format('first', 'second')
'Specify the order: second, first'
>>> 'Or name parameters: {first}, {second}'.format(second='SECOND', first='FIRST')
'Or name parameters: FIRST, SECOND'

In 95% of cases, this formatting will be all that's required; keeping things simple is great! But for complicated times, such as when aligning strings automatically and creating good looking text tables, the mini-language format has more options.

How to do it…

Write the following script, recipe_format_strings_step1.py, to print an aligned table:

# INPUT DATA
data = [
    (1000, 10),
    (2000, 17),
    (2500, 170),
    (2500, -170),
]
# Print the header for reference
print('REVENUE | PROFIT | PERCENT')
# This template aligns and displays the data in the proper format
TEMPLATE = '{revenue:>7,} | {profit:>+6} | {percent:>7.2%}'
# Print the data rows
for revenue, profit in data:
    row = TEMPLATE.format(revenue=revenue, profit=profit, percent=profit / revenue)
    print(row)

Run it to display the following aligned table. Note that PERCENT is correctly displayed as a percentage:

$ uv run recipe_format_strings_step1.py
REVENUE | PROFIT | PERCENT
  1,000 |    +10 |   1.00%
  2,000 |    +17 |   0.85%
  2,500 |   +170 |   6.80%
  2,500 |   -170 |  -6.80%

How it works…

The TEMPLATE constant defines three columns, each one defined by a parameter named revenue, profit, and percent respectively. This makes it explicit and straightforward to apply the template to the format call.

After the name of the parameter there's a colon that separates the format definition. Note that everything is inside the curly brackets. In all columns, the format specification sets the width to seven characters, to make sure all the columns have the same width, and aligns the values to the right with the > symbol:

  • revenue adds a thousands separator with the , symbol – [{revenue:>7,}].
  • profit adds a + sign for positive values. A minus sign (‑) for negatives is added automatically – [{profit:>+7}].
  • percent displays a percent value with a precision of two decimal places – [{percent:>7.2%}]. This is done through .2 (precision) and adding a % symbol for the percentage.

There's more…

You may have also seen the Python formatting available with the % operator. While it works for simple formatting, it is less flexible than the formatting mini-language, and it is not recommended that you use it.

A great new feature since Python 3.6 is to use f-strings, which perform a format action using defined variables:

>>> param1 = 'first'
>>> param2 = 'second'
>>> f'Parameters {param1}:{param2}'
'Parameters first:second'

This simplifies a lot of the code and allows us to create very descriptive and readable code.

Be careful when using f-strings to ensure that the string is replaced at the proper time. A common problem is that the variable defined to be rendered is not yet defined. For example, TEMPLATE, defined previously, won't be defined as an f-string, as revenue and the rest of the parameters are not available at that point. All variables defined at the scope of the string definition will be available, both local and global.

If you need to write a curly bracket, you'll need to repeat it twice. Note that each duplication will be displayed as a single curly bracket, plus a curly bracket for the value replacement, making a total of three brackets:

>> value = 'VALUE'
>>> f'This is the value, in curly brackets {{{value}}}'
'This is the value, in curly brackets {VALUE}'

This allows us to create meta templates – templates that produce templates. In some cases this will be useful, but they get complicated very quickly. Use with care, as it's easy to produce code that will be difficult to read.

Representing characters that have a special meaning usually requires some sort of special way to specify them, for example, by duplicating the curly bracket as we see here. This is called 'escaping' and it's a common process in any code representation.

The Python Format Specification mini-language has more options than the ones shown here.

As the language tries to be quite concise, sometimes it can be difficult to determine the position of the symbols. You may sometimes ask yourself questions, like 'is the + symbol before or after the width parameters?' Read the documentation with care and remember to always include a colon before the format specification – or, alternatively, ask the GenAI the proper formatting:

Prompt example:

How can I display in Python a number formatted so that it has a thousands separator, two decimal places and fits in a column of 9 rows, left aligned

Summarized example response:

f"{num:<9,.2f}"

Please refer to the full documentation and examples on the Python website: https://docs.python.org/3/library/string.html#formatspec or at the fantastic web page at https://pyformat.info, which shows lots of examples.

See also

  • The Template Reports recipe in Chapter 5, Generating Fantastic Reports, to learn more advanced template techniques.
  • The Manipulating strings recipe, later in this chapter, to learn more about working with text.

Manipulating strings

When dealing with text, it's often necessary to manipulate and process it; that is, to be able to join it, split it into regular chunks, or change it to uppercase or lowercase. We'll discuss more advanced methods for parsing text and separating it later; however, in lots of cases, it is useful to divide a paragraph into lines, sentences, or even words. Other times, words will require some characters to be removed or a word will need to be replaced with a canonical version to be able to compare it with a predetermined value.

Getting ready

We'll define a basic piece of text and transform it into its main components; then we'll reconstruct it. As an example, imagine a report needs to be transformed into a new format to be sent via email.

The input format we'll use in this example will be this:

    AFTER THE CLOSE OF THE SECOND QUARTER, OUR COMPANY, CASTAÑACORP
    HAS ACHIEVED A GROWTH IN THE REVENUE OF 7.47%. THIS IS IN LINE
    WITH THE OBJECTIVES FOR THE YEAR. THE MAIN DRIVER OF THE SALES HAS BEEN
    THE NEW PACKAGE DESIGNED UNDER THE SUPERVISION OF OUR MARKETING DEPARTMENT.
    OUR EXPENSES HAS BEEN CONTAINED, INCREASING ONLY BY 0.7%, THOUGH THE BOARD
    CONSIDERS IT NEEDS TO BE FURTHER REDUCED. THE EVALUATION IS SATISFACTORY
    AND THE FORECAST FOR THE NEXT QUARTER IS OPTIMISTIC. THE BOARD EXPECTS
    AN INCREASE IN PROFIT OF AT LEAST 2 MILLION DOLLARS.

We need to redact the text to eliminate any references to numbers. It needs to be properly formatted by adding a new line after each period, justified with 80 characters, and transformed into ASCII for compatibility reasons.

The text will be stored in the INPUT_TEXT variable in the interpreter.

How to do it…

  1. After entering the text, split it into individual words:
    >>> INPUT_TEXT = '''
    ...     AFTER THE CLOSE OF THE SECOND QUARTER, OUR COMPANY, CASTAÑACORP
    ...     HAS ACHIEVED A GROWTH IN THE REVENUE OF 7.47%. THIS IS IN LINE
    ...
    '''
    >>> words = INPUT_TEXT.split()
  2. Replace any numerical digits with an 'X' character:
    >>> redacted = [''.join('X' if w.isdigit() else w for w in word) for word in words]
  3. Transform the text into pure ASCII (note that the name of the company contains the letter ñ, which is not ASCII):
    >>> ascii_text = [word.encode('ascii', errors='replace').decode('ascii')
    ...               for word in redacted]
  4. Textwrap into 80-character lines:
    >>> newlines = [word + '\n' if word.endswith('.') else word for word in ascii_text]
    >>> LINE_SIZE = 80
    >>> lines = []
    >>> line = ''
    >>> for word in newlines:
    ...     if line.endswith('\n') or len(line) + len(word) + 1 > LINE_SIZE:
    ...         lines.append(line)
    ...         line = ''
    ...     line = line + ' ' + word
  5. Format all of the lines as titles and join them as a single piece of text:
    >>> lines = [line.title() for line in lines]
    >>> result = '\n'.join(lines)
  6. Print the result:
    >>> print(result)
     After The Close Of The Second Quarter, Our Company, Casta?Acorp Has Achieved A Growth In The Revenue Of X.Xx%. This Is In Line With The Objectives For The Year. The Main Driver Of The Sales Has Been The New Package Designed Under The Supervision Of Our Marketing Department. Our Expenses Has Been Contained, Increasing Only By X.X%, Though The Board Considers It Needs To Be Further Reduced. The Evaluation Is Satisfactory And The Forecast For The Next Quarter Is Optimistic.

How it works…

Each step performs a specific transformation of the text:

  • The first step splits the text into the default separators, whitespaces, and new lines. This splits it into individual words with no lines or multiple spaces for separation.
  • To replace the digits, we go through every character of each word. For each one, if it's a digit, an 'X' is returned instead. This is done with two list comprehensions, one to run on the list, and another on each word, replacing them only if there's a digit —['X' if w.isdigit() else w for w in word]. Note that the words are joined together again.
  • Each of the words is encoded into an ASCII byte sequence and decoded back again into the Python string type. Note the use of the errors parameter to force the replacement of unknown characters such as ñ.

    The difference between strings and bytes is not very intuitive at first, especially if you never have to worry about multiple languages or encoding transformations. Strings only exist in Python, as long as the information leaves the interpreter (for example, to be written in a file) is converted into bytes. In Python 3, there's a strong separation between internal strings and bytes. So, most of the tools applicable to strings won't be available in byte objects. Unless you have a good idea of why you need a byte object, always work with Python strings. If you need to perform transformations like the one in this task, encode and decode in the same line so that you keep your objects within the comfortable realm of Python strings. If you are interested in learning more about encodings you could refer to the brief article at https://eli.thegreenplace.net/2012/01/30/the-bytesstr-dichotomy-in-python-3, as well as the longer and more detailed one at http://www.diveintopython3.net/strings.html.

  • The next step adds an extra newline character (the \n character) for all words ending with a period. This marks the different paragraphs. After that, it creates a line and adds the words one by one. If an extra word will make it go over 80 characters, it finishes the line and starts a new one. If the line already ends with a new line, it finishes it and starts another one as well. Note that there's an extra space added to separate the words.
  • Finally, each of the lines is capitalized as a Title (the first letter of each word is uppercased) and all the lines are joined through new lines.

There's more…

Some other useful operations that can be performed on strings are as follows:

  1. Strings can be sliced like any other list. This means that "word"[0:2] will return "wo".
  2. Use .splitlines() to separate lines with a newline character.
  3. There are .upper() and .lower() methods, which return a copy with all of the characters set to uppercase or lowercase. Their use is very similar to .title():
    >>> 'UPPERCASE'.lower()
    'uppercase'
  4. For easy replacements (for example, changing all As to Bs or changing mine \ to ours), use .replace(). This method is useful for very simple cases, but replacements can get tricky easily. Be careful with the order of replacements to avoid collisions and case sensitivity issues. Note the wrong replacement in the following example:
    >>> 'One ring to rule them all, one ring to find them, One ring to bring them all and in the darkness bind them.'.replace('ring', 'necklace')
    'One necklace to rule them all, one necklace to find them, One necklace to bnecklace them all and in the darkness bind them.'

This is similar to the issues we'll encounter with regular expressions matching unexpected parts of your code. There are more examples to follow later. Refer to the regular expressions recipes for more information.

To wrap text lines you can use the textwrap module included in the standard library, instead of manually counting characters. View the documentation here: https://docs.python.org/3/library/textwrap.html.

If you work with multiple languages, or with any kind of non-English input, it is very useful to learn the basics of Unicode and encodings. In a nutshell, given the vast amount of characters in all the different languages in the world, including alphabets not related to the Latin one, such as Chinese or Arabic, Unicode provides a standard to try and cover all of them so that computers can properly understand them. Python 3 greatly improved this situation, making sure that the internal objects of the strings can deal with all of those characters. The default encoding that Python uses, and the most common and compatible one, is currently UTF-8.

A good article to learn about the basics of UTF-8 is this blog post: https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/.

Dealing with encodings is still relevant when reading from external files that can be encoded in different encodings (for example, CP-1252 or windows-1252, which is a common encoding produced by legacy Microsoft systems, or ISO 8859-15, which is the industry standard).

See also

  • The previous recipe, Creating strings with formatted values recipe, to learn the basics of string creation.
  • The Introducing regular expressions recipe, covered later in the chapter, to learn how to detect and extract patterns in text.
  • The Going deeper into regular expressions recipe, covered later in the chapter, to further your knowledge of regular expressions.
  • The Dealing with encodings recipe in Chapter 4, Searching and Reading Local Files, to learn about different kinds of encodings.

Extracting data from structured strings

In a lot of automated tasks, we'll need to treat input text structured in a known format and extract the relevant information. For example, a spreadsheet may mention a percentage in a piece of text (such as 37.4%) and we want to retrieve it in a numerical format to apply it later (0.374, as a float).

In this recipe, we'll learn how to process sale logs that contain inline information about a product, such as whether it has been sold, its price, profit made, and other information.

Getting ready

Imagine that we need to parse information stored in sales logs. We'll use a sales log with the following structure:

[<Timestamp in iso format>] - SALE - PRODUCT: <product id> - PRICE: $<price of the sale>

For example, a specific log may look like this:

[2018-05-05T10:58:41.504054] - SALE - PRODUCT: 1345 - PRICE: $09.99

Note that the price has a leading zero. All prices will have two digits for the dollars and two for the cents.

The standard ISO 8601 defines standard ways of representing the time and date. It is widely used in the computing world and can be parsed and generated by virtually any computer language.

How to do it…

  1. In the Python interpreter, make the following imports. Remember to run uv run python, which will use your virtualenv, as described in the section Working with multiple Python versions and virtual environments with uv:
    >>> import pendulum
    >>> from decimal import Decimal

    We will use Decimal type to keep precision at all times. More information later in the recipe

  2. Enter the log to parse:
    >>> log = '[2025-05-05T11:07:12.267897] - SALE - PRODUCT: 1345 - PRICE: $09.99'
  3. Split the log into its parts, which are divided by - (note the space before and after the dash). We ignore the SALE part as it doesn't add any relevant information:
    >>> divide_it = log.split(' - ')
    >>> timestamp_string, _, product_string, price_string = divide_it
  4. Parse the timestamp into a datetime object:
    >>> timestamp = pendulum.parse(timestamp_string.strip('[]'))
  5. Parse the product_id into an integer:
    >>> product_id = int(product_string.split(':')[-1])
  6. Parse the price into a Decimal type:
    >>> price = Decimal(price_string.split('$')[-1])
  7. Now you have all of the values in native Python format:
    >>> timestamp, product_id, price
    (DateTime(2025, 5, 5, 11, 7, 12, 267897, tzinfo=Timezone('UTC')), 1345, Decimal('9.99'))

How it works…

The basic working of this is to isolate each of the elements and then parse them into the proper type. The first step is to split the full log into smaller parts. The – (dash) string is a good divider, as it splits it into four parts—a timestamp one, one with just the word SALE, the product, and the price.

In the case of the timestamp, we need to isolate the ISO format, which is in brackets in the log. That's why the timestamp has the brackets stripped from it. We use the pendulum module (introduced earlier) to parse it into a DateTime object.

The word SALE is ignored. There's no relevant information there.

To isolate the product ID, we split the product part at the colon. Then we parse the last element as an integer:

>>> product_string.split(':')
['PRODUCT', ' 1345']
>>> int(' 1345')
1345

To divide the price, we use the dollar sign as a separator, and parse it as a Decimal character:

>>> price_string.split('$')
['PRICE: ', '09.99']
>>> Decimal('09.99')
Decimal('9.99')

As described in the next section, do not parse this value into a float type, as it will change the precision.

There's more…

These log elements can be combined together into a single object, helping to parse and aggregate them. For example, we could define a class in Python code in the following way:

class PriceLog(object):
  def __init__(self, timestamp, product_id, price):
    self.timestamp = timestamp
    self.product_id = product_id
    self.price = price
  def __repr__(self):
    return '<PriceLog ({}, {}, {})>'.format(self.timestamp,
                                            self.product_id,
                                            self.price)
  @classmethod
  def parse(cls, text_log):
    '''
    Parse from a text log with the format
    [<Timestamp>] - SALE - PRODUCT: <product id> - PRICE: $<price>
    to a PriceLog object
    '''
    divide_it = text_log.split(' - ')
    tmp_string, _, product_string, price_string = divide_it
    timestamp = pendulum.parse(tmp_string.strip('[]'))
    product_id = int(product_string.split(':')[-1])
    price = Decimal(price_string.split('$')[-1])
    return cls(timestamp=timestamp, product_id=product_id, price=price)

So, the parsing can be done as follows:

>>> log = '[2018-05-05T12:58:59.998903] - SALE - PRODUCT: 897 - PRICE: $17.99'
>>> PriceLog.parse(log)
<PriceLog (2018-05-05 12:58:59.998903+00:00, 897, 17.99)>

Avoid using float types for prices. Floats have precision problems that may produce strange errors when aggregating multiple prices, for example:

>>> 0.1 + 0.1 + 0.1
0.30000000000000004

Try these two options to avoid any problems:

  • Use integer cents as the base unit: This means multiplying currency inputs by 100 and transforming them into Integers (or whatever fractional unit is correct for the currency used). You may still want to change the base when displaying them.
  • Parse into the decimal type: The Decimal type keeps the fixed precision and works as you'd expect. You can find further information about the Decimal type in the Python documentation at https://docs.python.org/3.8/library/decimal.html.

If you use the Decimal type, parse the results directly into Decimal from the string. If transforming it first into a float, you can carry the precision errors to the new type.

Parsing data is also something where GenAI can help, but be sure to confirm that the results are as expected. In my experience GenAI tools will try to use regular expressions and not convert the types into the proper ones (e.g. not transforming prices into Decimal, but instead keeping them as strings or transforming them into float values)

See also

  • The Creating a virtual environment recipe, covered earlier in the chapter, to learn how to start a virtual environment with installed modules.
  • The next recipe, Using a third-party tool—parse, to further your knowledge of how to use third-party tools to deal with text.
  • The Introducing regular expressions recipe, later in the chapter, to learn how to detect and extract patterns from text.
  • The Going deeper into regular expressions recipe, later in the chapter, to further your knowledge of regular expressions.

Using a third-party tool—parse

While manually parsing data works very well for small strings, as seen in the previous recipe, it can be very laborious to tweak the exact formula to work with a variety of inputs. What if the input has an extra dash sometimes? Or if it has a variable length header depending on the size of one of the fields?

A more advanced option is to use regular expressions, as we'll see in the next recipe. But there's a great module in Python called parse (https://github.com/r1chardj0n3s/parse), which allows us to reverse format strings. It is a fantastic tool that's powerful, easy to use, and greatly improves the readability of code.

Getting ready

Add the parse module to the requirements.txt file in our virtual environment and reinstall the dependencies, as shown in the Creating a virtual environment recipe.

The requirements.txt file should look like this:

pendulum==3.2.0
requests== 2.32.5
parse==1.20.2

Then, reinstall the modules in the virtual environment:

$ uv pip install -r requirements.txt
Resolved 13 packages in 349ms
Prepared 1 package in 45ms
Installed 1 package in 4ms
 + parse==1.20.2

How to do it…

  1. Import the parse function:
    >>> from parse import parse
  2. Define the log to parse, in the same format as in the Extracting data from structured strings recipe:
    >>> LOG = '[2025-10-06T12:58:00.714611] - SALE - PRODUCT: 1345 - PRICE: $09.99'
  3. Analyze it and describe it as you would do when trying to print it, like this:
    >>> FORMAT = '[{date}] - SALE - PRODUCT: {product} - PRICE: ${price}'
  4. Run parse and check the results:
    >>> result = parse(FORMAT, LOG)
    >>> result
    <Result () {'date': '2025-10-06T12:58:00.714611', 'product': '1345', 'price': '09.99'}>
    >>> result['date']
    '2025-10-06T12:58:00.714611'
    >>> result['product']
    '1345'
    >>> result['price']
    '09.99'
  5. Note the results are all strings. Define the types to be parsed:
    >>> FORMAT = '[{date:ti}] - SALE - PRODUCT: {product:d} - PRICE: ${price:05.2f}'
  6. Parse once again, noticing that the types now match the defined ones:
    >>> result = parse(FORMAT, LOG)
    >>> result['date']
    datetime.datetime(2025, 10, 6, 12, 58, 0, 714611)
    >>> result['product']
    1345
    >>> result['price']
    9.99

Define a custom type for the price to avoid issues with the float type:

>>> from decimal import Decimal
>>> def price(string):
...   return Decimal(string)
...
>>> FORMAT = '[{date:ti}] - SALE - PRODUCT: {product:d} - PRICE: ${price:price}'
>>> parse(FORMAT, LOG, {'price': price})
<Result () {'date': datetime.datetime(2025, 10, 6, 12, 58, 0, 714611), 'product': 1345, 'price': Decimal('9.99')}>

Notice that now the price field is defined as Decimal.

How it works…

The parse module allows us to define a format, as a string, that reverses the format method when parsing values. A lot of the concepts that we discussed when creating strings apply here—put values in brackets, define the type after a colon, and so on.

By default, as seen initially, the values are parsed as strings. This is a good starting point when analyzing text. The values can be parsed into more useful native types, as shown next. Please note that while most of the parsing types are the same as the ones in the Python Format Specification mini-language, some others are available, such as ti for timestamps in ISO format.

Though we are using timestamp in this book in a more liberal way as a replacement for "Date and time," in the strictest sense, it should only be used for numeric formats, such as Unix timestamp or epoch, defined as the number of seconds since a particular time.

The usage of a timestamp that includes other formats is common anyway as it's a clear and understandable concept, but be sure to agree on formats when sharing information with others.

If native types are not enough, our own parsing can be defined, as demonstrated in the last step. Note that the definition of the price function gets a string and returns the proper format, which in this case a Decimal type.

All the issues about floats and price information described in the There's more… section of the Extracting data from structured strings recipe apply here as well.

There's more…

The timestamp can also be translated into a pendulum object for consistency. Also, pendulum objects carry over time zone information. Adding the same structure as in the previous recipe gives the following object, which is capable of parsing logs:

import parse
from decimal import Decimal
import pendulum
class PriceLog(object):
  def __init__(self, timestamp, product_id, price):
    self.timestamp = timestamp
    self.product_id = product_id
    self.price = price
  def __repr__(self):
    return '<PriceLog ({}, {}, {})>'.format(self.timestamp,
                                            self.product_id,
                                            self.price)
  @classmethod
  def parse(cls, text_log):
    '''
    Parse from a text log with the format
    [<Timestamp>] - SALE - PRODUCT: <product id> - PRICE: $<price>     to a PriceLog object
   '''
    def price(string):
      return Decimal(string)
    def isodate(string):
      return pendulum.parse(string)

    FORMAT = ('[{timestamp:isodate}] - SALE - PRODUCT: {product:d} - '
              'PRICE: ${price:price}')
    formats = {'price': price, 'isodate': isodate}
    result = parse.parse(FORMAT, text_log, formats)
    return cls(timestamp=result['timestamp'],
               product_id=result['product'],
               price=result['price'])

So, parsing it returns similar results:

>>> log = '[2025-10-06T14:58:59.051545] - SALE - PRODUCT: 827 - PRICE: $22.25'
>>> PriceLog.parse(log)
<PriceLog (2025-10-06 14:58:59.051545+00:00, 827, 22.25)>

This code is contained in the GitHub file, https://github.com/PacktPublishing/Python-Automation-Cookbook-Third-Edition/blob/main/ch01/price_log.py

All supported parse types can be found in the documentation at https://github.com/r1chardj0n3s/parse#format-specification.

Typically, GenAI tries to use regexes, which we will see in the next two recipes, to parse values. That works in some cases, but it can overcomplicate things. We will see some of the problems of regexes later. Having an easier way to parse regular values is very valuable and should not be underestimated.

See also

  • The Extracting data from structured strings recipe, earlier in this chapter, to learn how to use simple processes to get information from text.
  • The next recipe, Introducing regular expressions, to learn how to detect and extract patterns from text.
  • The Going deeper into regular expressions recipe, later in this chapter, to further your knowledge of regular expressions.

Introducing regular expressions

A regular expression, or regex, is a pattern to match text. In other words, it allows us to define an abstract string (typically, the definition of a structured kind of text) to check against other strings to see if they match or not.

It is better to describe them with an example. Think of defining a pattern of text, such as a word that starts with an uppercase A and contains only lowercase 'n's and 'a's after that. Let's show some possible comparisons and results:

Text to compare

Result

Anna

Match

Bob

No match (No initial A)

Alice

No match (l is not n or a after initial A)

James

No match (No initial A)

Aaan

Match

Ana

Match

Annnn

Match

Aaaan

Match

ANNA

No match (N is not n or a)

Table 1.1: A pattern matching example

If this sounds complicated, that's because it is. Regexes can, notoriously, be complicated, intricate and difficult to follow. But they are also very useful because they allow us to perform incredibly powerful pattern matching.

Some common uses of regexes are:

  • Validating input data: For example, a phone number consisting only of numbers, dashes, and brackets.
  • String parsing: Retrieve data from structured strings, such as logs or URLs. Typically, capture some section of the full string. This is similar to what's described in the previous recipe.
  • Scraping: Find the occurrences of something in a long piece of text. For example, find all the email addresses in a web page.
  • Replacement: Find and replace a word or words with others. For example, replace the owner with John Smith.

Getting ready

The Python module to deal with regexes is called re. The main function we'll cover is re.search(), which returns a match object with information about what matched the pattern.

As regex patterns are also defined as strings, we'll differentiate them by prefixing them with an r, such as r'pattern'. This is the Python way of labeling a text as raw string literals, meaning that the string within is taken literally, without any escaping. This means that a "\" is used as a backslash instead of an escaping sequence. For example, without the r prefix, \n means a newline character.

Some characters are special and refer to concepts such as the end of the string, any digit, any character, any whitespace character, and so on.

The simplest form is just a literal string. For example, the regex pattern r'LOG' matches the string 'LOGS', but not the string 'NOT A MATCH'. If there's no match, re.search returns None. If there is, it returns a special Match object:

>>> import re
>>> re.search(r'LOG', 'LOGS')
<_sre.SRE_Match object; span=(0, 3), match='LOG'>
>>> re.search(r'LOG', 'NOT A MATCH')
>>>

How to do it…

  1. Import the re module:
    >>> import re
  2. Then, to match a pattern that is not at the start of the string:
    >>> re.search(r'LOG', 'SOME LOGS')
    <_sre.SRE_Match object; span=(5, 8), match='LOG'>
  3. Match a pattern that is only at the start of the string. Note the ^ character at the start of the pattern:
    >>> re.search(r'^LOG', 'LOGS')
    <_sre.SRE_Match object; span=(0, 3), match='LOG'>
    >>> re.search(r'^LOG', 'SOME LOGS')
    >>>
  4. Match a pattern only at the end of the string. Note the $ character at the end:
    >>> re.search(r'LOG$', 'SOME LOG')
    <_sre.SRE_Match object; span=(5, 8), match='LOG'>
    >>> re.search(r'LOG$', 'SOME LOGS')
    >>>
  5. Match the word 'thing' (not excluding things), but not something or anything. Note the \b at the start of the second pattern:
    >>> STRING = 'something in the things she shows me'
    >>> match = re.search(r'thing', STRING)
    >>> STRING[:match.start()], STRING[match.start():match.end()], STRING[match.end():]
    ('some', 'thing', ' in the things she shows me')
    >>> match = re.search(r'\bthing', STRING)
    >>> STRING[:match.start()], STRING[match.start():match.end()], STRING[match.end():]
    ('something in the ', 'thing', 's she shows me')
  6. Match a pattern that's only numbers and dashes (for example, a phone number). Retrieve the matched string:
    >>> re.search(r'[0123456789-]+', 'the phone number is 1234-567-890') <_sre.SRE_Match object; span=(20, 32), match='1234-567-890'>
    >>> re.search(r'[0123456789-]+', 'the phone number is 1234-567-890').group()
    '1234-567-890'
  7. Match an email address naively:
    >>> re.search(r'\S+@\S+', 'my email is email.123@test.com').group() 'email.123@test.com'

How it works…

The re.search function matches a pattern, no matter its position in the string. As explained previously, this will return None if the pattern is not found, or a Match object.

The following special characters are used:

  • ^: Marks the start of the string
  • $: Marks the end of the string
  • \b: Marks the start or end of a word
  • \S: Marks any character that's not a whitespace, including characters like * or $

More special characters are shown in the next recipe, Going deeper into regular expressions.

In step 6 of the How to do it… section, the r'[0123456789‑]+' pattern is composed of two parts. The first is between square brackets, and matches any single character between 0 and 9 (any number) and the dash (‑) character. The + sign after that means that this character can be present one or more times. This is called a quantifier in regexes. This makes a match on any combination of numbers and dashes, no matter how long it is.

Step 7 again uses the + sign to match as many characters as necessary before the @ and again after it. In this case, the character match is \S, which matches any non-whitespace character.

Please note that the naive pattern for emails described here is very naive, as it will match invalid emails such as john@smith@test.com. A better regex for most uses is r"(^[a‑zA‑Z0‑9_.+‑]+@[a‑zA‑Z0‑9‑]+\.[a‑zA‑Z0‑9‑.]+$)". You can go to http://emailregex.com/ to find it, along with links to further information.

Note that parsing a valid email including corner cases is actually a difficult and challenging problem. The previous regex should be fine for most uses covered in this book, but specific tools like specific the library email‑validator (https://github.com/JoshData/python-email-validator) or tools in a general framework project such as Django, are better options to handle emails.

The resulting matching object returns the position where the matched pattern starts and ends (using the start and end methods), as shown in step 5, which splits the string into matched parts, showing the distinction between the two matching patterns.

The difference displayed in step 5 is a very common one. Trying to capture GP (as in General Practitioner, for a medical doctor) can end up capturing eggplant and bagpipe! Similarly, thing\b won't capture things. Be sure to test and make the proper adjustments, such as capturing \bGP\b for just the word GP.

The specific matched pattern can be retrieved by calling group(), as shown in step 6. Note that the result will always be a string. It can be further processed using any of the methods we've seen previously, such as by splitting the phone number into groups by dashes, for example:

>>> match = re.search(r'[0123456789-]+', 'the phone number is 1234-567-890')
>>> [int(n) for n in match.group().split('-')]
[1234, 567, 890]

The captured string will include all characters, like the dashes in this case. Note how we remove them by using .split()

There's more…

Dealing with regexes can be difficult and complex. Please allow time to test your matches and be sure that they work as you expect in order to avoid nasty surprises.

"Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems."

– Jamie Zawinski

Regular expressions are at their best when they are kept very simple. In general, if there is a specific tool to do it, prefer it over regexes. A very clear example of this is with HTML parsing; refer to Chapter 3, Building Your First Web Scraping Application, for better tools to achieve this.

Some text editors allow us to search using regexes as well. While most are editors aimed at writing code, such as Vim, BBEdit, or Notepad++, they're also present in more general tools, such as MS Office, Open Office, or Google Documents. But be careful, as the particular syntax may be slightly different.

You can check your regexes interactively. A good one that's freely available online is https://regex101.com/, which displays each of the elements and explains the regex. Double-check that you're using the Python flavor:

Figure 1.2: An example using RegEx101

Figure 1.2: An example using RegEx101

Note that the EXPLANATION box in the preceding image describes that \b matches a word boundary (the start or end of a word), and that thing matches literally these characters.

Regexes, in some cases, can be very slow, or even susceptible to what's called a regex denial-of-service attack, a string created to confuse a particular regex so that it takes an enormous amount of time. In the worst-case scenario, it can even block the computer. While automating tasks probably won't get you into those problems, keep an eye out in case a regex takes too long to process.

GenAI is quite keen on regexes and it will try to parse most text with regexes. This is generally a problem and it's a good idea to critically analyze any regex and double check if there's a simpler way to retrieve the same information. There are many tools that allow simpler parsing of common elements that can produce safer, more understandable code.

At the same time, GenAI can be very useful when producing regex if that's what's required.

The bottom line is that regexes can be difficult to maintain and to test that they produce the required effect. Take care with them.

See also

  • The Extracting data from structured strings recipe, earlier in the chapter, to learn simple techniques to extract information from text.
  • The Using a third-party tool—parse recipe, earlier in the chapter, to use a third-party tool to extract information from text.
  • The Going deeper into regular expressions recipe, covered next, to further your knowledge of regular expressions.

Going deeper into regular expressions

In this recipe, we'll learn more about how to deal with regular expressions. After introducing the basics, we will dig a little deeper into pattern elements, introduce groups as a better way to retrieve and parse strings, learn how to search for multiple occurrences of the same string, and deal with longer texts.

How to do it…

  1. Import re:
    >>> import re
  2. Match a phone pattern as part of a group (in brackets). Note the use of \d as a special character for any digit:
    >>> match = re.search(r'the phone number is ([\d-]+)', '37: the phone number is 1234-567-890')
    >>> match.group()
    'the phone number is 1234-567-890'
    >>> match.group(1)
    '1234-567-890'
  3. Compile a pattern and capture a case-insensitive pattern with a yes|no option:
    >>> pattern = re.compile(r'The answer to question (\w+) is (yes|no)', re.IGNORECASE)
    >>> pattern.search('Naturally, the answer to question 3b is YES')
    <_sre.SRE_Match object; span=(10, 42), match='the answer to question 3b is YES' >
    >>> pattern.search('Naturally, the answer to question 3b is YES').groups()
    ('3b', 'YES')
  4. Match all the occurrences of cities and state abbreviations in the text. Note that they are separated by a single character, and the name of the city always starts with an uppercase letter. Only four states are matched for simplicity:
    >>> PATTERN = re.compile(r'([A-Z][\w\s]+?).(TX|OR|OH|MI)')
    >>> TEXT ='the jackalopes are the team of Odessa,TX while the knights are native of Corvallis OR and the mud hens come from Toledo.OH; the whitecaps have their base in Grand Rapids,MI'
    >>> list(PATTERN.finditer(TEXT))
    [<_sre.SRE_Match object; span=(31, 40), match='Odessa,TX'>, <_sre.SRE_Match object; span=(73, 85), match='Corvallis OR'>, <_sre.SRE_Match object; span=(113, 122), match='Toledo.OH'>, <_sre.SRE_Match object; span=(157, 172), match='Grand Rapids,MI'>]
    >>> _[0].groups() ('Odessa', 'TX')

How it works…

The new special characters that were introduced are as follows:

  • \d: Matches any digit (0 to 9).
  • \s: Matches any character that's a whitespace, including tabs and other whitespace special characters. Note that this is the reverse of \S, which was introduced in the previous recipe.
  • \w: Matches any letter (this includes digits, but excludes characters such as periods).
  • .: (dot): Matches any character.

Note that the same letter in uppercase or lowercase means the opposite match, for example, \d matches a digit, while \D matches a non-digit.

To define groups, put them in parentheses. A group can be retrieved individually, making it perfect for matching a bigger pattern that contains a variable part to be processed in the next step, as demonstrated in step 2. Note the difference from the step 6 pattern in the previous recipe. In this case, the pattern is not only the number, but it includes the prefix text, even if we then extract only the number:

>>> re.search(r'the phone number is ([\d-]+)', '37: the phone number is 1234-567-890')
<_sre.SRE_Match object; span=(4, 36), match='the phone number is 1234-567-890'>
>>> _.group(1)
'1234-567-890'
>>> re.search(r'[0123456789-]+', '37: the phone number is 1234-567-890')
<_sre.SRE_Match object; span=(0, 2), match='37'>
>>> _.group()
'37'

Remember that group 0 (.group() or .group(0)) is always the whole match. The rest of the groups are ordered as they appear.

Patterns can be compiled as well. This saves some time if the pattern needs to be matched over and over. To use it that way, compile the pattern and then use the compiled object to perform searches, as shown in steps 3 and 4. Some extra flags can be added, such as making the pattern case insensitive.

Step 4's pattern requires a little bit of explanation. It's composed of two groups, separated by a single character. The special character "." (dot) means it matches everything. In our example, it matches a period, a whitespace, and a comma. The second group is a straightforward selection of defined options, in this case, US state abbreviations.

The first group starts with an uppercase letter ([A‑Z]) and accepts any combination of letters or spaces ([\w\s]+?), but not punctuation marks such as periods or commas. This matches the cities, including those that are composed of more than one word.

The final +? makes the match of letters non-greedy, matching as few characters as possible. This avoids some problems such as when there are no punctuation symbols between the cities. Take a look at the result where we don't include the non-greedy qualifier for the second match and how it includes two elements:

>>> PATTERN = re.compile(r'([A-Z][\w\s]+).(TX|OR|OH|MI)')
>>> TEXT ='the jackalopes are the team of Odessa,TX while the knights are native of Corvallis OR and the mud hens come from Toledo.OH; the whitecaps have their base in Grand Rapids,MI'
>>> list(PATTERN.finditer(TEXT))[1]
<re.Match object; span=(73, 122), match='Corvallis OR and the mud hens come from Toledo.OH>

Note that this pattern starts on any uppercase letter and keeps matching until it finds a state, unless separated by a punctuation mark, which may not be what's expected, for example:

>>> re.search(r'([A-Z][\w\s]+?).(TX|OR|OH|MI)', 'This is a test, Escanaba MI')
<_sre.SRE_Match object; span=(16, 27), match='Escanaba MI'>
>>> re.search(r'([A-Z][\w\s]+?).(TX|OR|OH|MI)', 'This is a test with Escanaba MI')
<_sre.SRE_Match object; span=(0, 31), match='This is a test with Escanaba MI'>

Step 4 also shows you how to find more than one occurrence in a long text. While the .findall() method exists, it doesn't return the full match object, while .finditer() does. As is common now in Python 3, .finditer() returns an iterator that can be used in a for loop or list comprehension. Note that .search() returns only the first occurrence of the pattern, even if more matches appear:

>>> PATTERN.search(TEXT)
<_sre.SRE_Match object; span=(31, 40), match='Odessa,TX'>
>>> PATTERN.findall(TEXT)
[('Odessa', 'TX'), ('Corvallis', 'OR'), ('Toledo', 'OH'), ('Grand Rapids', 'MI')]

There's more…

The special characters can be reversed if they are case swapped. For example, the reverse of the ones we used are as follows:

  • \D: Marks any non-digit.
  • \W: Marks any non-letter.
  • \B: Marks a position that's not at the start or end of a word. For example, r'thing\B' will match things but not thing.

The most commonly used special characters are typically \d (digits) and \w (letters and digits), as they match digits and letters, which are normally what you want.

Groups can be assigned names as well. This makes them more explicit at the expense of making the group more verbose in the following way— (?P<groupname>PATTERN). Groups can be referred to by name with .group(groupname) or by calling .groupdict() while maintaining its numeric position.

For example, the step 4 pattern can be described as follows:

>>> PATTERN = re.compile(r'(?P<city>[A-Z][\w\s]+?).(?P<state>TX|OR|OH|MN)')
>>> match = PATTERN.search(TEXT)
>>> match.groupdict()
{'city': 'Odessa', 'state': 'TX'}
>>> match.group('city')
'Odessa'
>>> match.group('state')
'TX'
>>> match.group(1), match.group(2)
('Odessa', 'TX')

As perhaps you have by now come to suspect, regular expressions are a very extensive topic. They can be notoriously deep, and there are whole technical books devoted to them, like Mastering Python Regular Expressions by Lopez and Romero. The Python documentation is also a good reference to use (https://docs.python.org/3/library/re.html) and to learn more.

GenAI is your friend here! GenAI loves regexes and will explain in great detail how an existing regex works and how to change it if there's an unexpected effect.

Prompt example:

Explain this regex r'(?P<city>[A‑Z][\w\s]+?).(?P<state>TX|OR|OH|MN)'

Summarized example response:

(?P<city>[A‑Z][\w\s]+?) This is a named captured book named city. It should start with a capital letter.

. A literal dot. It captures a single character. If the intent was to match a literal period, it should be written as "\."

(?P<state>TX|OR|OH|MN) This is another named capturing group called state.

It matches exactly one of these state abbreviations: TX, OR, OH or MN

Potential gotchas:

The dot will match any character and not just a literal dot.

Cities with punctuation (like St. Paul) may not work correctly

If you feel a little intimidated at the start, it's a perfectly natural feeling. Starting by asking GenAI. It will analyze the patterns with care, and they will start to make sense. You can keep asking the AI about it. In addition, don't be afraid to run a regex interactive analyzer!

While regexes can be really powerful and generic, they may not be the proper tool for what you are trying to achieve. We've seen some caveats already, and patterns that have subtleties. As a rule of thumb, if a pattern starts to feel complicated, it's time to search for a different tool. That's a common experience with GenAI code, where the tool will likely try to overuse regexes. Remember the previous recipes too and the options they presented, such as parse.

See also

  • The Introducing regular expressions recipe, earlier in the chapter, to learn the basics of using regular expressions.
  • The Using a third-party tool—parse recipe, earlier in the chapter, to learn a different technique to extract information from text.

Adding command-line arguments

Often a solution is best structured as a command-line interface that accepts different parameters to change the way it works, for example, scraping a web page from a provided URL or other URL. Python includes a powerful argparse module in the standard library to create rich command-line argument parsing with minimal effort.

argparse is included in the standard library, and it is a solid choice for common line scripts. But great third-party tools are also available, like click (https://click.palletsprojects.com/en/stable/) and typer (https://typer.tiangolo.com) that are worth exploring. They offer additional features for more sophisticated CLI interfaces.

Getting ready

The basic use of argparse in a script can be shown in three steps:

  1. Define the arguments that your script is going to accept, generating a new parser.
  2. Call the defined parser, returning an object with all of the resulting arguments.
  3. Use the arguments to call the entry point of your script, which will apply the defined behavior.

Try to use the following general structure for your scripts:

IMPORTS
def main(main parameters):
  DO THINGS
if __name__ == '__main__':
    DEFINE ARGUMENT PARSER
    PARSE ARGS
    VALIDATE OR MANIPULATE ARGS, IF NEEDED
    main(arguments)

The main function makes it easy to know what the entry point for the code is. The section under the if statement is only executed if the file is called directly, but not if it's imported. We'll follow this for all the steps.

How to do it…

  1. Create a script that will accept a single integer as a positional argument, and will print a hash symbol that number of times. The recipe_cli_step1.py script is as follows, but note that we are following the structure presented previously, and the main function is just printing the argument:
    import argparse
    def main(number):
        print('#' * number)
    if __name__ == '__main__':
        parser = argparse.ArgumentParser()
        parser.add_argument('number', type=int, help='A number')
        args = parser.parse_args()
    
        main(args.number)
  2. Call the script and check how the parameter is presented. Calling the script with no arguments displays the automatic help. Use the automatic argument ‑h to display the extended help:
    $ uv run recipe_cli_step1.py
    usage: recipe_cli_step1.py [-h] number
    recipe_cli_step1.py: error: the following arguments are required: number
    $ uv run recipe_cli_step1.py -h
    usage: recipe_cli_step1.py [-h] number
    
    positional arguments:
      number                A number
    
    options:
      -h, --help            show this help message and exit
  3. Calling the script with the extra parameters work as expected
    $ uv run recipe_cli_step1.py 4
    ####
    $ uv run recipe_cli_step1.py not_a_number
    usage: recipe_cli_step1.py [-h] number
    recipe_cli_step1.py: error: argument number: invalid int value: 'not_a_number'
  4. Change the script to accept an optional argument for the character to print. The default will be "#". The recipe_cli_step2.py script will look like this:
    import argparse
    def main(character, number):
        print(character * number)
    if __name__ == '__main__':
        parser = argparse.ArgumentParser()
        parser.add_argument('number', type=int, help='A number')
        parser.add_argument('-c', type=str, help='Character to print',
                            default='#')
    args = parser.parse_args()
    main(args.c, args.number)
  5. The help is updated, and using the ‑c flag allows us to print different characters:
    $ uv run recipe_cli_step2.py -h
    usage: recipe_cli_step2.py [-h] [-c C] number
    
    positional arguments:
      number                A number
    
    options:
      -h, --help            show this help message and exit
      -c C                  Character to print
    $ uv run recipe_cli_step2.py 4
    ####
    $ uv run recipe_cli_step2.py 5 -c m
    mmmmm
  6. Add a flag that changes the behavior when present. The recipe_cli_step3.py script is as follows:
    import argparse
    def main(character, number):
        print(character * number)
    if __name__ == '__main__':
        parser = argparse.ArgumentParser()
        parser.add_argument('number', type=int, help='A number')
        parser.add_argument('-c', type=str, help='Character to print',
                            default='#')
        parser.add_argument('-U', action='store_true', default=False,
                            dest='uppercase',
                            help='Uppercase the character')
        args = parser.parse_args()
        if args.uppercase:
            args.c = args.c.upper()
        main(args.c, args.number)
  7. Calling it uppercases the character if the ‑U flag is added:
    $ uv run recipe_cli_step3.py 4
    ####
    $ uv run recipe_cli_step3.py 4 -c f
    ffff
    $ uv run recipe_cli_step3.py 4 -c f -U
    FFFF

How it works…

As described in step 1 of the How to do it… section, the arguments are added to the parser through .add_arguments. Once all of the arguments are defined, calling parse_args() returns an object that contains the results (or exits if there's an error).

Each argument should add a help description, but their behavior can change greatly depending on the details:

  • If an argument starts with a ‑, it is considered an optional parameter, like the ‑c argument in step 4. If not, it's a positional argument, like the number argument in step 1.
  • For clarity, always define a default value for optional parameters. It will be None if you don't, but this may be confusing. It's a good idea to add default=None to be explicit.
  • Remember to always add a help parameter with a description of the parameter; help is automatically generated, as shown in step 2.
  • If a type is present, it will be validated, for example, number in step 3. By default, the type will be a string.
  • The actions store_true and store_false can be used to generate flags, arguments that don't require any extra parameters. Set the corresponding default value to be the opposite Boolean. This is demonstrated in the U argument in steps 6 and 7.
  • The name of the property in the args object will be, by default, the name of the argument (without the dash, if it's present). You can change it with dest. For example, in step 6, the command-line argument ‑U is described as uppercase.

Changing the name of an argument for internal usage is very useful when using short arguments, such as single letters. A good command-line interface will use ‑c, but, internally, it's probably a good idea to use a more verbose label, such as configuration_file. Remember, explicit is better than implicit!

Some arguments can work in coordination with others, as shown in step 3. Prepare the parameters to pass to the main function so they are clear. For example, in step 3, only two parameters are passed, but one may have been modified.

There's more…

You can create long arguments as well with double dashes, for example:

parser.add_argument('-v', '--verbose', action='store_true', default=False,
    help='Enable verbose output')

This will accept both ‑v and ‑‑verbose, and it will store the name verbose.

Adding long names is a good way of making the interface more intuitive and easier to remember.

The main inconvenience when dealing with command-line arguments is that you may end up with too many of them. This creates confusion. Try to make your arguments as independent as possible and don't make too many dependencies between them; otherwise, handling the combinations can be tricky.

In particular, try to not create more than a couple of positional arguments, as they won't have mnemonics. Positional arguments also accept default values, but most of the time, that won't be the expected behavior.

GenAI can help you produce a complex interface if necessary, or speed up its production by defining the expected use case. But keep in mind that a complex interface is likely an indication of trying to perform a complex operation with too many parameters, and that a rethink is needed. Do you really need all those parameters? Can it be simplified?

To simplify the usage of the command-line interface, but still allow further customization, you can use environment variables to set default values – as for example in this code (recipe_cli_env.py):

import argparse
import os

default_character = os.environ.get('DEFAULT_CHAR', '#')


def main(character, number):
    print(character * number)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('number', type=int, help='A number')
    help_msg = f'Character to print (default "{default_character}"). Set with env DEFAULT_CHAR'
    parser.add_argument('-c', type=str, help=help_msg,
                        default=default_character)
    args = parser.parse_args()
    main(args.c, args.number)

Notice how we use the dictionary os.environ to retrieve a value in the environment variable DEFAULT_CHAR. If not present, it will use the value '#'. We store it into default_character, which we use to define the default value in the parser. We include extra information in the help command to make clear how to use the parameters and environment variables.

Because it uses the environment variable, which can be defined in the shell, the need to always specify the character to use when we run it is removed:

$ uv run recipe_cli_env.py 4
####
$ DEFAULT_CHAR=* uv run recipe_cli_env.py 4
****
$ DEFAULT_CHAR=* uv run recipe_cli_env.py -c ^ 5
^^^^^

This pattern is common in command-line utilities and can be used to simplify the call, while allowing for flexibility.

For advanced details on argparse, refer to the Python documentation (https://docs.python.org/3/library/argparse.html).

See also

  • The Creating a virtual environment recipe, earlier in this chapter, to learn how to create an environment installing third-party modules.
  • The Installing third-party packages recipe, earlier in this chapter, to learn how to install and use external modules in the virtual environment.

Get this book's PDF version and more

Scan the QR code (or go to packtpub.com/unlock). Search for this book by name, confirm the edition, and then follow the steps on the page.

Image

Image

Note: Keep your invoice handy. Purchases made directly from Packt don't require an invoice.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Apply proven Python automation recipes to solve real-world tasks
  • Integrate AI models and build intelligent agents for business workflows
  • Extend automation across systems using MCP and modern integrations

Description

Automating repetitive tasks and integrating systems efficiently becomes increasingly complex as workflows scale. This book helps you solve that problem with practical Python recipes that guide you from foundational automation to advanced, AI-powered workflows. You start by building a strong base in Python automation, exploring tested solutions for file handling, web scraping, APIs, testing, and system operations, and learning how to design reliable automation workflows. The cookbook approach enables you to quickly apply solutions to real problems while building a deeper understanding through hands-on practice. This third edition expands the scope of automation by introducing AI-powered capabilities. You learn how to call AI models within your scripts, use and implement the Model Context Protocol (MCP) for system integration, and design intelligent agents that automate decision-making processes. New chapters provide real-world examples of AI agents in business automation, helping you move beyond scripts to adaptive systems. This book combines practical knowledge with modern techniques to ensure you stay current with evolving automation practices. By the end of this book, you will be able to design, build, and extend Python automation workflows, including AI-driven solutions, to handle complex real-world tasks with confidence.

Who is this book for?

Python developers, automation engineers, DevOps professionals, and system administrators who want to streamline workflows and integrate modern AI capabilities into automation. A basic understanding of Python programming is recommended.

What you will learn

  • Automate file, system, and network tasks using Python
  • Build robust scripts for web scraping and API integration
  • Design scalable automation workflows for real use cases
  • Integrate AI models into automation pipelines
  • Implement MCP for system-level automation integration
  • Develop intelligent agents for business automation
  • Apply testing and debugging techniques for automation
  • Create real-world AI-driven automation solutions

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Jun 29, 2026
Length: 676 pages
Edition : 3rd
Language : English
ISBN-13 : 9781806702886
Category :
Languages :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Product Details

Publication date : Jun 29, 2026
Length: 676 pages
Edition : 3rd
Language : English
ISBN-13 : 9781806702886
Category :
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts

Table of Contents

19 Chapters
Chapter 1: Let's Begin Our Automation Journey Chevron down icon Chevron up icon
Chapter 2: Automating Tasks Made Easy Chevron down icon Chevron up icon
Chapter 3: Building Your First Web Scraping Application Chevron down icon Chevron up icon
Chapter 4: Searching and Reading Local Files Chevron down icon Chevron up icon
Chapter 5: Generating Fantastic Reports Chevron down icon Chevron up icon
Chapter 6: Fun with Spreadsheets Chevron down icon Chevron up icon
Chapter 7: Cleaning and Processing Data Chevron down icon Chevron up icon
Chapter 8: Developing Stunning Graphs Chevron down icon Chevron up icon
Chapter 9: Dealing with Communication Channels Chevron down icon Chevron up icon
Chapter 10: Why Not Automate Your Marketing Campaign? Chevron down icon Chevron up icon
Chapter 11: Calling AI Models Chevron down icon Chevron up icon
Chapter 12: Adding Tools to GenAI Chevron down icon Chevron up icon
Chapter 13: Building AI Agents for Business Automation Chevron down icon Chevron up icon
Chapter 14: Real-World AI Example Chevron down icon Chevron up icon
Chapter 15: Automatic Testing Routines Chevron down icon Chevron up icon
Chapter 16: Debugging Techniques Chevron down icon Chevron up icon
Chapter 17: Unlock Your Exclusive Benefits Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.

Modal Close icon
Modal Close icon