Python ascii(): Built-in Function to Enforce ASCII-Printable Object Representation

python ascii function banner overcoded

The Python ascii() function returns a printable ASCII-escaped string representation of an object. This function is part of the Python built-ins and works in close concert with the repr() function, an Object’s __repr__() and __str__() methods, and offers developers greater control over how Object models are represented in string form.

The ascii() function in Python is not used to obtain the ASCII value for a character. Rather, it is used to represent an Object as an ASCII-formatted string to the greatest extent possible. It escapes non-ASCII characters to their hexadecimal Unicode values. These values are then inserted into the original string alongside valid ASCII characters.

TL;DR – The ascii() function ensures an object can be represented as a printable ASCII-formatted string by substituting for any non-ASCII characters representing that object.

# Call the ascii() function on a string with
# a mix of ASCII and non-ASCII characters
>>> ascii("""ðvêr¢oÐeÐ""")

# Valide ASCII characters returned
# along with /x escaped hex values
# for non-ASCII valid characters
'\xf0v\xear\xa2o\xd0e\xd0'

Basic Use Cases

The ascii() function ensures that an object’s string representation contains only printable ASCII characters. For any non-ASCII characters, the ascii() function will substitute an escaped sequence of hexadecimal characters. This function can be used with any object including strings, collections, and custom object models. Let’s take a look at some of the ascii() function’s basic uses.

# Create a list with some random values
# containing all ASCII characters
a = ['string', 1, 5.773, 'overcoded']
print(a)

['string', 1, 5.773, 'overcoded']

# Create a list with some values
# containing some non-ASCII characters
o = ['striñg', "Ø", 'ØvercØded']
print(ascii(o))

['stri\xf1g', '\xd8', '\xd8verc\xd8ded']

# Create a custom object with an
# ASCII format name
print(ascii(type('CustomObject', (object,), {'name': 'custom object'})()))

<__main__.CustomObject object at 0x000001E5691AF6D0>

# Create a custom object with an
# nont-ASCII format name
print(ascii(type('CüstomØbject', (object,), {'name': 'custom object'})()))

<__main__.C\xfcstom\xd8bject object at 0x000001E5691AF6D0>

Note that for any non-ASCII character the ascii() function is substituting a sequence of characters starting with \x. This is the standard Python escape character for a leading hexadecimal-escaped value. The formal format is \xhh where the two h’s are hexadecimal-valid characters (a-f; 0-9) or any combination.

Some of this may look like nonsense still. Most notably, it’s not exactly clear how the ascii() function is using the non-ASCII characters used in defining our custom object’s name. That’s how why ØvercØded is printed out as \xd8verc\xd8ded. The two Ø characters are escaped to their Unicode-mapped values using the hexadecimal value \x + d8.

This is still a little confusing in my opinion. For starters, it’s not immediately clear where ascii() is getting the value that it is choosing to evaluate for ASCII compliance. To tease apart that question we’ll have to take a quick detour to consider Python’s approach to data modeling.

Crash Course: Python’s Data Model

Python’s  ascii() function is related closely to Python’s data model and how it controls textual representations of data/objects. This article focuses on the ascii() function but we will need to touch on some basics of how Python’s data model works to fully understand how to use the ascii() function. I promise it’ll be as brief as possible.

Python’s data model uses a __repr__() method that is tied into the built-in repr() function. Together, these work in concert to control how objects are visually represented during processes like printing to console, logging, or other string-based contexts. The official __repr__() documentation describes the method as such:

Called by the repr() built-in function to compute the “official” string representation of an object. The return value must be a string object. If a class defines __repr__() but not __str__(), then __repr__() is also used when an “informal” string representation of instances of that class is required.

Let’s pick that apart, just a bit. This description hints that the __repr__() and __str__() methods of Python’s data model are very related. Here’s what you need to know:

  1. __str__() uses the value of __repr__() unless explicitly defined.
  2. __repr__() is used for debugging and contains pertinent info including object type and memory address.
  3. __str__() is a casual representation of an object.
  4. __repr__() is a formal representation of an object.
  5. The ascii() function uses the repr() function, which uses the __repr__() of an object, as its data source.

Class-Based Usage

We can now consider use cases of Python’s ascii() function given our quick crash course on Python’s data modeling. I’m going to create a very basic class to demonstrate how the ascii() function uses an object’s __repr__() method to provide a valid ASCII format string representation of that object.

class Dude:
    """A custom class for Dude's"""
    def __init__(self, name: str = 'John Doe'):
        self.name = name


# Create a new Dude named Bob
bob = Dude(name='Bob')

# View the type and default repr of bob
print(type(bob))
print(bob)

<__main__.Dude object at 0x00000158D03A6940>
<class '__main__.Dude'>

# View the default __str__ value of bob
print(bob.__str__())
print(type(bob.__str__()))

<__main__.Dude object at 0x0000020114426940>
<class 'str'>

# View the default __repr__ value for bob
print(bob.__repr__())
print(type(bob.__repr__()))

<__main__.Dude object at 0x0000020114426940>
<class 'str'>

# View the type and value of bob as ascii
print(type(ascii(bob)))
print(ascii(bob))

<__main__.Dude object at 0x0000020114426940>
<class 'str'>

We’re getting str values all-around it would seem. This behavior isn’t relevant to the ascii() function yet because we’re still using only valid ASCII characters in our representations of Bob. Let’s make another Dude that’s a little more worldly:

# Create another dude using non-ASCII characters
rob = Dude(name="Ròb")

# View the default repr of Ròb
print(type(rob))
print(rob)

<class '__main__.Dude'>
<__main__.Dude object at 0x0000013423F2F2E0>

We’ve added a non-ASCII character into our new Dude object but it hasn’t changed anything. Now, let’s see what tweaking our object’s __str__() method will change:

class Dude:
    """A custom class for Dude's"""
    def __init__(self, name: str = 'John Doe'):
        self.name = name

    def __str(self):
        """
        A custom method to explicitly define
        the string representation of a Dude
        """
        return self.name

# Create another dude using non-ASCII characters
rob = Dude(name="Ròb")

# View the default repr of Ròb
print(type(rob))
print(rob)
print(ascii(rob))

<class '__main__.Dude'>
Ròb
<__main__.Dude object at 0x000001EB52914AC0>

Aha! We can now see that when an object explicitly defines a __str__() method that value will override the object’s default __repr__() value. However, when using the ascii() function to get an ASCII-printable version of our Rob object things are still bland. That’s because the ascii() methods uses the value of __repr__() even if __str__() is explicitly defined. That’s important to keep in mind for later.

We’ve altered our Dude object model in such a way that printing to console will afford us a friendlier representation. We can still fall back to the object’s __repr__() method for debugging should we need information like memory address or object type. Being this responsable doesn’t really let us stretch the legs of Python’s built-in ascii() function. Let’s alter the __repr__() method and see what happens!

class Dude:
    """A custom class for Dudes"""
    def __init__(self, name: str = 'John Doe'):
        self.name = name

    def __str__(self):
        """
        A custom method to explicitly define
        the informal string representation of a Dude
        """
        return self.name
    
    def __repr__(self):
        """
        A custom method to explicitly define
        the formal string representation of a Dude
        """
        return self.__str__()

# Create another dude using non-ASCII characters
rob = Dude(name="Ròb")

# View the default repr of Ròb
print(type(rob))
print(rob)
print(ascii(rob))

<class '__main__.Dude'>
Ròb
R\xf2b

The ascii() function now uses the __str__() value of Rob because we’ve told our Dude object model’s __repr__() method to return the __str__ value. This now feeds our object’s non-ASCII name into the ascii() function via the object’s __repr__ value and results in an ASCII-escaped, the printable value of R\xf2b. The \xf2b value is an escaped hexadecimal value assigned to the Unicode character ò. Now we’re juggling several layers of Objects, instances, built-ins, and defaults. Let’s take a moment to visualize this to make sure we’re keeping things straight.

Visualizing Python’s ascii() Function

ascii illustration python
Python’s ascii() function uses the __repr__ value of an object for the source data

Python’s ascii() function can be incredibly useful when one is aware of the flow of data of the object model. Without knowing where the ascii() function is getting its data the function can be quite frustrating. Using the above illustration and the following attribute’s list, I hope to help make the source of the ascii() function’s data a bit clearer

  1. __str__ uses __repr__ by default
  2. __str__ can be overridden, without affecting __repr__
  3. ascii() uses the value of __repr__ regardless of what’s going on with __str__
  4. ascii() can use the value of __str__ if __repr__ explicitly returns that value.
    1. ascii() is still using the value of __repr__, it’s just that __repr__ is then using the value of __str__

Now that we have a solid grasp on how data is flowing between these functions, attributes, and our object let’s really have some fun with the ascii() function. Those wary of violating PEP-approved naming conventions may want to stop reading here!

Non-ASCII Character Representations

We’ve seen that overriding the __repr__ method can affect the return value of Python’s ascii() function when used on our Dude objects. We’ve also seen that returning the value from our Dude object’s __str__() method via its __repr__() method can result in the ascii() function providing its designed escaping functionality. Let’s now consider how the ascii() function might behave if we used non-ASCII characters in defining our class.

class Düde:
    """A cüstom class for Düdes"""
    def __init__(self, name: str = 'John Doe'):
        self.name = name


# Create a new Düde named Büd
büd = Düde(name='Büd')

# Print the str and repr
<__main__.Düde object at 0x000002A911931FD0>
<__main__.Düde object at 0x000002A911931FD0>
<__main__.Düde object at 0x000002A911931FD0>
<__main__.D\xfcde object at 0x000002A911931FD0>

In this class, I’ve substituted our boring domesticated u for a more exciting umlaut-ornamented ü. This non-ASCII character will display fine in most consoles but certainly isn’t within the range of printable ASCII characters. Note how the ü displays in its encoded form in our Object’s class name (sorry PEP) via the __repr__ value. Now note that the __repr__ value, using the default Object model value taken from the class name Düde, has the ü escaped via the ascii() function: D\xfcde.

Review

Did you ever think there could be so much to know about such a simple built-in function? Python’s ascii() function is a great gatekeeper function for cases where one wants to ensure avoidance of errors when logging to files that are to be printed or on systems where fuller ranges of encoding may not be available.

The ascii() function, along with the other built-ins, are one of the many reasons that Python continues to be one of the most widely-used programming languages in the world. In my experience, it’s not a function designed for everyday use among us casual developers. I find it to be a useful tool in better understanding the dynamics of Python’s data modeling nonetheless!

Zαck West
Full-Stack Software Engineer with 10+ years of experience. Expertise in developing distributed systems, implementing object-oriented models with a focus on semantic clarity, driving development with TDD, enhancing interfaces through thoughtful visual design, and developing deep learning agents.