Converting Your Python

This wiki page provides some specific information related to converting scripts from Python 2 to Python 3.  There is a great deal of information available online (e.g., here and here) related to the differences between the versions and gives advice on how to convert.  This wiki is not meant to be a comprehensive source on the subject, but rather a starting place and source for information of particular interest to Abaqus scripting users.

Back to the main Python 3 Update Wiki

Automatic Conversion Tool

Abaqus 2024 includes a tool for converting scripts from Python 2 to Python 3.  It is largely based on Python's native 2to3 conversion tool but adds a number of additional functionalities to facilitate creation of scripts that will be cross-compatible between versions.  The conversion tool can be invoked directly from the command line as follows:

 python -m abqPy2to3 

A few notes on this utility:

  • If you provide a directory, it recursively traverses through that directory and subdirectories looking for *.py files
  • The utility writes a log file and saves the original version of files it operates on.
  • You can supply “-future False” if you're not worried about maintaining Python 2.7 forwards compatibility (this will leave out the "from __future__ import print_function" statement that enables Python 2.7 to run with certain Python 3 syntax)

This script can also be accessed as part of CAE’s script upgrade plugin utility. To use it more interactively with the ability to preview the changes, start CAE and go to Plug-Ins→Abaqus→Upgrade Scripts… It is run if you upgrade scripts to 2024.

In general, the new syntax that Python 3 introduces was also added to Python 2.  For instance, the functionalization of the print command was also supported in Python 2 versions - Python 2.7 supports both the print statement and the built-in print() function.  In this way, it's often possible to write code that runs in both versions.  However, in general it may be a better practice to maintain separate codes for Python 2 and Python 3.  This is the recommendation at the end of Guido van Rossum's article on Python 3.

Occasionally, however, it may be more convenient to create code designed to run in both versions - such as within an abaqus_v6.env file that you want to function regardless of which Abaqus version you're running.  When writing such multi-version code, it may be necessary to branch certain sections of code based on the version of the Python interpreter that is running. If this is necessary, it can be accomplished by checking the version_info.major member in the sys module.  Here is one example that could be useful in an abaqus_v6.env file - using branching to specify a plugin directory that contains Abaqus/CAE plugins that are written for one version of Python or another:

import sys, os

myPluginDir = r"C:\users\me\abaqus_plugins_py3"
if sys.version_info.major < 3:  # Py2 (2023x and earlier)
    myPluginDir = r"C:\users\me\abaqus_plugins_py2"

# Add plugin dir to plugin_central_dir
if globals().get('plugin_central_dir'):
    if not myPluginDir in plugin_central_dir.split(os.pathsep):
        plugin_central_dir += os.pathsep + myPluginDir
else:
    plugin_central_dir = myPluginDir
del myPluginDir

Common Issues

The following is a discussion of common issues that arise when converting Abaqus Python scripts.

Strings

Strings have changed significantly in Python 3, and in ways that can be difficult to automatically convert.  In Python 2, the str object holds a byte string and the unicode object holds Unicode text.  If you're just creating strings without paying attention to things like Unicode, you're probably dealing with str objects.  In Python 3, strings (the str object) are explicitly Unicode and there is a new bytes object for storing data as bytes (usually ASCII characters).  You'll still probably be dealing with a str object if you're dealing with strings without paying attention to Unicode, but the str object is now what unicode was in Python 2. In Python 2, it was usually possible to mix unicode and str objects, but Python 3 will complain when str and bytes objects are mixed together.  In Python 3, Unicode strings (str objects) can be specified by defining a string in the normal manner (and prepending a unicode string with the character u is no longer required, although still supported to maintain backwards compatibility).  Byte strings (bytes objects) can be specified by prepending the string with the character b.

>>> textString =   'Hello' # This is a str object
>>> byteString =  b'Hello' # This is a bytes object
>>> print('UnicodeString'==u'UnicodeString') # These are the same, "u" is implicit for strings
True

Many issues arising from mixing of bytes and str objects in Python 3 manifest as TypeError exceptions being raised.  This change impacts quite a bit of functionality around strings, including the following.

  • File I/O - It matters a great deal more whether a file is opened in binary or text mode (text is the default).
    • Reads from files opened in binary mode yield bytes objects, and values written to these files must be bytes objects.
    • Reads from files opened in text mode yield str objects, and values written to these files must be str objects.
    • Attempting to write a str object to a binary file or a bytes object to a text file will raise a TypeError exception.
  • String formatting
    • The str type supports the format() method, but bytes does not.
    • Both str and bytes support the % operator for formatting, but both operands of % must be of the same type.
  • Conversion between str and bytes in Python 3
    • bytes object can be converted to a str object using the decode() method
    • str object can be converted to a bytes object using the encode() method. 
    • The encoding format can be defined by the user; the default is 'utf-8'.
  • Checking if something is a string
    • The StringType member of the types module is removed in Python 3.  Instead, use isinstance(,str), assuming that the object you're checking is a str and not bytes or something else.
  • Other modules and functionality - In Python 3, it is important to note whether a module or function inputs or outputs str or bytes as this impacts how one inputs data and uses output
    • ​​​​​​​​​​​​​​The struct module results in bytes objects in Python 3
>>> textString =  'Hello' # This is a str object
>>> byteString = b'Hello' # This is a bytes object
>>> print(textString==byteString) # dissimilar types, will evaluate to false
False
>>> print(textString==byteString.decode()) # decode byteString to str object
True
>>> print(textString+byteString) # concatenating str and bytes gives TypeError
Traceback (most recent call last):
  File "", line 1, in 
TypeError: can only concatenate str (not "bytes") to str

Lists and Iterables

Many built-in functions that previously returned list objects now return iterables. Iterables can be advantageous in terms of memory requirements, and their behavior should be the same when simply looping over them, but there are some important differences.  Importantly, it's not possible to directly index into an iterable object as it is for a list.  Note that this isn't the whole story - there are a number of specifics related to certain outputs like the range object returned by the range() function, or the "views" that are returned by the keys() and items() methods of dictionary objects.  By default, the conversion utility wraps the results of methods for which this behavior changed in the list() function to ensure compatibility. Often this isn’t necessary and may result in less optimal code (but typically no less optimal than the original code that natively used lists in Python 2).

>>> easyas = [1,2,3]
>>> simpleas = ['a','b','c']
>>> result = zip(easyas,simpleas) # Creates an zip object (an iterable of tuples)
>>> print(result)

>>> result[1] # Cannot index into iterable
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'zip' object is not subscriptable
>>> list(result)[1]
(2, 'b')

Comparisons

Python 3 is much stricter regarding comparisons.  In general, attempts to compare variables of different types will result in TypeError exceptions being raised.  The == operator will return False when it doesn't know how to compare two differing types.  Also, the cmp() function is gone from Python 3.

>>> print("1">100)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: '>' not supported between instances of 'str' and 'int'
>>> print("1"==1) # string and int are incomparable types, == evaluates to False
False
>>> print("1"==str(1)) # converting integer to a string for consistent types
True
>>> print(1.0==1) # Python can compare floats and integers
True

Integer Division

In Python 2, the / operator performs integer division if both operands are integers.  In Python 3, the / operator is always floating point division.  The // operator explicitly performs integer division in both Python 2.7 and Python 3.  It is not possible to detect whether integer division is intended or not, so this must be carefully checked by script developers.  Note that this issue also impacts the evaluation of *PARAMETER data lines.

>>> 5/2 # In Python 2 this would have been integer division
2.5
>>> 4/2 # The / operator always returns a float
2.0
>>> 5//2 # The // operator performs integer division
2
>>> # Note that if any operand of // is a float, 
>>> # it returns the integer division result as a float
>>> 5.0//2 
2.0

Code Execution with exec

The exec statement in Python 2 is changed to the exec() function in Python 3.  Also execfile() was removed in Python 3, but it is included in the miscUtils module in Abaqus Python 3.  The way that exec() interacts with local variables when called within a function block also changes in Python 3.  Consider the following snippet of Python code that utilizes exec(), once at the module namespace level and another time within a function:

print("Run at module level:")
a=7
exec("a+=1\nprint(a)")
print(a)
def call_exec():
    a_loc = 17
    exec("a_loc+=1\nprint(a_loc)")
    print(a_loc)
print("Run in function:")
call_exec()

The result of this script will be different in Python 2 and Python 3.  Note that in Python 3, the variable a_loc defined at the start of the call_exec() function does not get modified by the call to exec()

Python 2Python 3
Run at module level:
8
8
Run in function:
18
18
Run at module level:
8
8
Run in function:
18
17

The options to work around this limitation with exec() in Python 3 are limited.  One possible option is to pass in the locals dictionary to the exec() call and then update the local variables of interest after the call is completed, as follows.

print("Run at global level:")
a=7
exec("a+=1\nprint(a)")
print(a)
def call_exec():
    a_loc = 17
    l_dict = locals().copy()
    exec("a_loc+=1\nprint(a_loc)",globals(),l_dict)
    a_loc = l_dict['a_loc']    
    print(a_loc)
print("Run in function:")
call_exec()

It is not possible to arbitrarily update the local variables in the function based on the call to exec() as one could do in Python 2.  See more information here.

Feedback

We expect that many Abaqus users will be going through the process of updating Python scripts.  Feel free to comment below with any insights you think might be helpful to others as they take on this task!