A Developer’s Guide to Python 3.0: Package Installation and More Language Enhancements

A Developer’s Guide to Python 3.0: Package Installation and More Language Enhancements

he previous articles in this “Developer’s Guide to Python 3.0” series covered the major changes to the language and the standard library. This article covers some of the more esoteric changes and enhancements, along with a few lower-impact changes.

PEP-370: The New Per-User Site-Packages Directory

Until version 2.5, Python had a single site-packages directory into which it installed packages. Except for the Mac Framework build, this directory wasn’t controlled by the user—so users without admin privileges weren’t able to install packages to the site-packages directory; they had to request IT department help (and you how well that works).

Workarounds included using alternative installation schemes (such as --home or --prefix installation modes) and you could use PYTHONPATH to add a directory to the package lookup process, but these weren’t exactly the same as relying on site-packages because they ignore .pth files.

More flexible solutions are available, such as virtualenv, which solves a much broader problem and allows you to have multiple isolated Python environments, albeit at the expense of a little administration (you have to activate and deactivate the virtual environment), but PEP-370 takes a different approach. In addition to the standard site-packages it also supports a per-user site-packages directory where packages can be installed.

Here’s how all the options play out if you install the same package (anysql-0.0.5) in various ways on a Mac OS X laptop, removing any previously installed copy of the package before each fresh installation. It’s worth noting that the official documentation on installing Python modules doesn’t mention PEP-370 and per-user site-packages.

Here’s the standard installation for anysql:

~/Contributors/Gigi/anysql-0.0.5 > python3.0 installrunning installrunning buildrunning build_pyrunning install_libcopying build/lib/ -> /Library/Frameworks/Python.framework/Versions/3.0/lib/python3.0/   site-packagesbyte-compiling /Library/Frameworks/Python.framework/   Versions/3.0/lib/python3.0/site-packages/ to anysql.pycrunning install_egg_infoWriting /Library/Frameworks/Python.framework/Versions/   3.0/lib/python3.0/site-packages/anysql-0.0.5-py3.0.egg-info

At this point, the anysql package has been installed successfully to the standard site-packages directory: /Library/Frameworks/Python.framework/Versions/3.0/lib/python3.0/site-packages, and I was able to import it just fine.

Author’s Note: The egg-info in the last line is additional metadata installed by setuptools-based packages, and is not important for this discussion.

Using the --home scheme, the installation looks like this:

~/Contributors/Gigi/anysql-0.0.5 > python3.0 install --home=~running installrunning buildrunning build_pyrunning install_libcreating /Users/gsayfan/libcreating /Users/gsayfan/lib/pythoncopying build/lib/ -> /Users/gsayfan/lib/pythonbyte-compiling /Users/gsayfan/lib/python/ to anysql.pycrunning install_egg_infoWriting /Users/gsayfan/lib/python/anysql-0.0.5-py3.0.egg-info

The preceding output shows that the anysql package has been installed to ~/lib/python. To import it I had to add this directory to my sys.path:

>>> sys.path.append(os.path.expanduser('~/lib/python'))>>> import anysql

Now, here’s the --prefix scheme installation:

~/Contributors/Gigi/anysql-0.0.5 > python3.0 install --prefix=~running installrunning buildrunning build_pyrunning install_libcreating /Users/gsayfan/lib/python3.0creating /Users/gsayfan/lib/python3.0/site-packagescopying build/lib/ -> /Users/gsayfan/lib/python3.0/site-packagesbyte-compiling /Users/gsayfan/lib/python3.0/site-packages/ to anysql.pycrunning install_egg_infoWriting /Users/gsayfan/lib/python3.0/site-packages/anysql-0.0.5-py3.0.egg-info

As you can see, it’s very similar to the home scheme, except that the package gets installed to ~/lib/python3.0/site-packages. To import it I had to add this directory to my sys.path:

>>> sys.path.append(os.path.expanduser('~/lib/python3.0/site-packages'))>>> import anysql

In general, the --prefix scheme lets you control more aspects of the installation.

Per-User Site-Packages

Finally, here’s the new per-user scheme. On the Mac OS X, packages get installed to ~/.local/lib/python3.0/site-packages. Unfortunately the ~/.local directory and all its subdirectories are owned by root, which means you need sudo access to actually install into this directory.

sudo python3.0 install --userrunning installrunning buildrunning build_pyrunning install_libcopying build/lib/ -> /Users/gsayfan/.local/lib/python3.0/site-packagesbyte-compiling /Users/gsayfan/.local/lib/python3.0/site-packages/ to anysql.pycrunning install_egg_infoWriting /Users/gsayfan/.local/lib/python3.0/site-packages/anysql-0.0.5-py3.0.egg-info

One nice thing about the --user scheme is that the user-specific site-packages directory is already in sys.path.

Overall, the user-specific site-packages scheme seems to target a very small population. There are many other existing solutions (virtual environments, portable python, PYTHONPATH,, --home/--prefix installation) that seem to address the same problem area. The added value of the per-user site packages solution is that you don’t have to alter sys.path, and it handles .pth files correctly.

PEP-343: The with Statement

The with statement was introduced in Python 2.5 as a future feature (required “from future import with_statement“). It is now officially part of the language. The with statement facilitates acquiring and releasing a resource in a block of code. This is similar to the C++ RAII (Resource Acquisition Is Initialization), where a resource is acquired in the constructor of a local (on the stack) object and released in the destructor. It is useful for expensive resources that you want to release as soon as possible, without waiting for Python’s GC to kick in. It also guarantees that the resource will be released. The syntax of the with statement is:

with EXPR as VAR:  BLOCK

The words with and as are now keywords (as is a “double” keyword also used in the try-except clause). EXPR is any expression that returns a context manager, which is an object that has __enter__ and __exit__ methods. The __enter__() method gets called before the BLOCK is executed and __exit__() gets called after the block is executed—even if block execution raises an exception. Here’s a timing context manager example. This context manager prints the elapsed time for the block to execute:

import timeclass timeit:  def __enter__(self):    self.start = time.time()  def __exit__(self, type, value, traceback):    print ('Elapsed:', time.time() - self.start)

You use it with the with keyword:

with timeit():  for i in range(1000):    open('1.txt', 'w',).write(str(i) * 2500)    Output:Elapsed: 0.996018886566

Obviously, this is a very simple context manager; it ignores exceptions, and the code inside the block doesn’t use the context manager itself. But that’s not always the case. For example, the built-in open function (which is actually as you may recall) is now a context manager that closes the file automatically when the scope is exited:

>>> with open('1.txt') as f:...   print('size of',, 'is:',  len( size of 1.txt is: 7500>>> f>>> (most recent call last):  File "", line 1, in   File "/Library/Frameworks/Python.framework/    Versions/3.0/lib/python3.0/", line 1721,     in read self._checkClosed()  File "/Library/Frameworks/Python.framework/    Versions/3.0/lib/python3.0/", line 450,     in _checkClosed    if msg is None else msg)ValueError: I/O operation on closed file.

This code illustrates a couple of interesting points:

  • The open file is bound to the variable f.
  • The file f was closed when the with clause finishes (hence the exception when trying to read it).

It is interesting that the variable f is still bound to the now closed file. It would have been nicer if the binding of the name f had been scoped to the block, so that trying to access f outside the with block would result in a NameError. I think this would better match the semantics of with, where resources exist only inside the with block.

Property Decorators

Property decorators help with writing properties. A property is one or more methods that can be accessed like a variable. Here is the classic property syntax as of Python 2.2:

class A:  def __init__(self):    self._x = 7  def _getX(self):    return self._x  def _setX(self, x):    assert isinstance(x, int)    self._x = x  def _delX(self, x):    del self._x  x = property(_getX, _setX, doc='x is an integer')

This class verifies that you set x to an int, and allows you to access x as a variable:

>>> a = A()>>> a.x7>>> a.x = 5>>> a.x  5>>> a.x = 3.4Traceback (most recent call last):  File "", line 1, in   File "", line 27, in setX    assert isinstance(x, int)AssertionError

You can even get help on x (via the class A and not the instance a):

>>> help(A.x)Help on property:    x is an integer

All this is great, but properties do have some annoyances. First, you have to have a separate line to define the property, and second, there’s no standard way to name the getter and setter (and deleter if you have one). The new property decorator solves these problems. Here’s the same class using property decorators:

class A:  def __init__(self):    self._x = 7  @property  def x(self):    """'x' is an integer"""    return self._x  @x.setter  def x(self, x):    assert isinstance(x, int)    self._x = x  @x.deleter  def x(self):    del self._x

Note that all the property accessors are now simply named x. The getter is decorated with @property and its doc string becomes the property docstring. The setter and deleter are decorated with @.setter and @.deleter respectively. To summarize, the property decorator is not earth-shattering but brings a little order and consistency to property definitions. I definitely plan to adopt this syntax for property definitions. The old syntax still works—and is necessary if you want to define a write-only property (no getter) with a doc string.

PEP-3102: Keyword-Only Arguments

Python always supported keyword arguments. But, it wasn’t possible to have keyword-only arguments. Given the following function:

def f(a, b, c='default'):  pass

You must provide the a and b arguments when calling f, either as positional arguments (a is first and b is second), or using keyword arguments:

# Calling f with a and b as positional argumentsf(3, 4)# Calling f with a and b as keyword argumentsf(a=3, b=4)# Order doesn't matter when using keyword argumentsf(b=4, a=3)

The c argument is optional because it has a default value. You can provide the c argument either as a positional argument or as a keyword argument:

f(3, 4, 5)f(3, 4, c=5)f(3, b=4, c=5)

So far, so good. Python also supports functions with a variable number of arguments via a vararg argument, which is simply a list:

def f(*args):  print(sum(args))  f(1, 2, 3)Output:6

The problem is that up to Python 2.5 it wasn’t possible to specify named keyword arguments after the vararg argument:

# This is illegaldef f(a, b, *args, d=4):  pass

You could have a dictionary of keyword arguments after the vararg, but the keyword arguments would have to be extracted from the dictionary and will have no default value.

def f(*args, **kw):  pass

Python 3.0 provides a new syntax to support keyword-only arguments following vararg in the function signature. There are two cases:

  1. You have zero or more fully-specified positional arguments preceding a vararg variable that’s followed by keyword-only arguments.
  2. >>> def f(a, b, *c, d=5):...   print(a, b, c, d)... >>> f(1,2,3)1 2 (3,) 5>>> f(1,2,3,4,5)1 2 (3, 4, 5) 5>>> f(1,2,d=4)1 2 () 4>>> f(1,d=4)Traceback (most recent call last):  File "", line 1, in TypeError: f() takes at least 2 non-keyword positional arguments (1 given)>>> f(1,d=4, b=2)1 2 () 4

    As you can see, a and b must be passed to f() either by position or by name. There may be additional positional arguments after a and b. Finally, d can be passed by name only (otherwise it will be considered a vararg). If you don’t pass both a and b you will get a TypeError.

  3. You have zero or more fully-specified positional arguments followed immediately by keyword-only arguments (no varargs):
  4. >>> def f(a, b, *, d):...   print (a, b, d)... >>> f(1,2,3)Traceback (most recent call last):  File "", line 1, in TypeError: f() takes exactly 2 positional arguments (3 given)>>> f(1,2,d=3)1 2 3>>> f(a=1,2, d=3)  File "", line 1SyntaxError: non-keyword arg after keyword arg>>> f(1, d=3, b=2)1 2 3

The lone asterisk marks the end of the positional arguments (a and b) and the start of the keyword-only arguments (d).

Another interesting observation is that both positional arguments and keyword-only argument may or may not have a default value. The only caveat is that for both positional arguments and keyword-only arguments you may not have a non-default argument follow an argument with a default. Here’s a function where the positional argument has a default and the keyword argument doesn’t:

>>> def f(x=4, *, y):...   print(x, y) ... >>> f(y=3)4 3>>> f(y=2, 6)  File "", line 1SyntaxError: non-keyword arg after keyword arg>>> f(6, y=5)6 5>>> f(y=5, x=7)7 5

This function requires that you specify y by name. You may omit x altogether because it has a default value (4). If you do pass x as a positional argument it must come before the y keyword-only argument. If you pass x by name too, then you can pass it after the y argument.

In all cases you may also have the catchall **keywords dictionary of optional keyword arguments. Here is the ultimate function that has positional arguments (with and without a default), varargs, keyword-only argument and a keywords dict:

>>> def f(a, b=2, *c, d, **e):...   print (a, b, c, d, e)...>>> f(1,2,3,d=4,e=5)1 2 (3,) 4 {'e': 5}>>> f(1,2,3,e=5, d=4)1 2 (3,) 4 {'e': 5}

PEP-3104: Access to Names in Outer Scope

Python supports nested scopes via nested functions. Names that are bound in a function scope are also visible in nested functions and classes.

def outer(n):  def inner():    print n  inner()external(3)Output:3

In Python 2.x, nested functions and classes couldn’t modify outer scope variables because trying to do so would simply create a new local variable in the nested function, without modifying the outer variable:

def outer(n):  def inner():    n = 5    print 'inner: n=', n  inner()  print 'outer: n=', nouter(3)Output:inner: n= 5outer: n= 3

Python did have global scope variables that could be modified inside functions and even nested functions:

n = 0def outer():  def inner():    global n    n = 7  inner()  print nouter()Output:7

In contrast, Python 3.0 allows nested functions and classes to modify non-global outer names by using the new keyword nonlocal. Its usage is similar to global:

def outer():  n = 6  def inner():    nonlocal n    n = 9  inner()  print(n)outer()

Note, that the code first declares nonlocal n and then in the next line assigns it a value. The PEP claims that you can do it in one line but the interpreter disagrees:

    nonlocal n = 9               ^SyntaxError: invalid syntax

This behavior matches the global keyword. It would be nice if both nonlocal and global could be used with the shorthand one-liner.

Here’s a more comprehensive example of nonlocal used in a class defined inside a function (yes, that’s allowed). The outer function defines a class named “A” internally, containing a nested function called makeA() that creates an instance of A. The outer function also accepts an argument n, which is in its scope of course. The A.__init__ method increments this number by one and prints the resulting value. Finally, the outer function returns the makeA function as its return value. The reason for this complicated arrangement is to show how name binding works.

After defining the outer function has been defined, the code calls it twice, with n=3 and n=7, and assigns the results to the variables make_1 and make_a2. Remember, the return value of outer is the nested function makeA(), so make_a1 and make_a2 are functions that create an instance of the inner class A (which is not visible from outside the outer function, and can’t be created directly). The interesting thing is that make_a1 and make_a2 are not identical. They both create an instance of the same class A, but the nonlocal binding is different. This is demonstrated below by calling make_a1() three times in a row:

def outer(n):  class A(object):    def __init__(self):      nonlocal n      n += 1      print(n)  def makeA():    return A()  return makeAmake_a1 = outer(3)make_a2 = outer(7)make_a1()make_a1()make_a1()print('---')make_a2()make_a2()Output:456---89

As the preceding output shows, the three calls to make_a1 produces the results 4, 5, and 6 (initially n=3 and it gets incremented three times) and then calling make_a2() twice, resulting in 8 and 9 (initially n=7 and it gets incremented twice).

Python C API Changes

Welcome to the land of the Python C API. Standard Python (aka CPython) is implemented in C (albeit very object-oriented C). The implementation language is relevant to a serious Python programmer because Python can be extended with extension modules written in C. There are two main reasons to use an extension module:

  1. You have a C library that already does exactly what you need, and you don’t want to rewrite it in Python.
  2. Speed.

Python itself (the CPython implementation) is very slow. This is partly due to its extreme flexibility, partly due to some design decisions, and partly because Python developers have always been able to speed up critical parts of their programs by replacing them with lightning-fast C extension modules.

Python 3.0 made several changes to the C API, which means that Python C extensions compiled against the Python 2.x API don’t work in Python 3.0.

Author’s Note: I will discuss this topic in depth in the next article in the series. I will not cover all the changes, due to time and space constraints, but will focus on two major changes that received their own PEPs, and explain them at a relatively high-level.

PEP-3121: Extension Module Initialization and Finalization

C extension modules are dynamically loaded libraries. In Python 2.x the life cycle of extension modules is a little problematic:

  1. They are generally initialized once and never get unloaded.
  2. The entry point that the Python interpreter calls to initialize an extension module is called init{module name}. This generic name must be exported, and can conflict with other global symbols.
  3. When Py_Finalize() is called the init function is called a second time. This is surprising and required careful management on the part of the extension developer to make sure all resources were cleaned up the second time.
  4. The init entry point has no return value. That deviates from common practice and doesn’t allow developers to check whether the initialization failed.
  5. If you run multiple interpreters that import the same extension module, it will be shared by all interpreters. This means that one interpreter can corrupt the state of another interpreter.

PEP-3121 solves all these issues. The entry point signature of the init function is now:

PyObject * PyInit_{module name}()

That solves the name-conflict issue and provides a return value. If initialization succeeds, the function returns a pointer to the module object, otherwise it returns NULL.

This function should return a new module object every time it’s called and each Python interpreter should call it once. The module object will be passed to each module function, allowing a separate state for each interpreter.

The module is defined in the following C struct:

struct PyModuleDef{  PyModuleDef_Base m_base;  /* To be filled out by the interpreter */  Py_ssize_t m_size; /* Size of per-module data */  PyMethodDef *m_methods;  inquiry m_reload;  traverseproc m_traverse;  inquiry m_clear;  freefunc m_free;};

The m_clear function gets called every time the GC clears the module’s memory (or NULL if not keeping state). The m_free function gets called when the module is deallocated (or NULL if not needed).

Unfortunately, Python 3.0 didn’t fix the reload use case. The Python reload() function from Python 2.x was even removed (it never worked properly). I’m not sure what the reason is. It seems that with per-module state and the free function it should be possible to implement reload() properly. The problem probably stems from the issue of modules being shared between interpreters. If you reload a module with modified code in one interpreter, what would happen to the module in another interpreter? The correct solutions are either not to share modules between interpreters to begin with (which could become some kind of an import option) or, whenever a module gets reloaded, load a new copy to the reloading interpreter and let other interpreters continue to run with the old module.

PEP-3118: Revising the Buffer Protocol

Many Python objects and modules share memory under the covers at the C level. Python 2.x provided a buffer protocol to allow shared access via pointer to a contiguous memory buffer or a sequence of segments. C API objects could export their data in raw format or consume other objects’ data. The Python 2.x buffer protocol had some deficiencies that this PEP addresses. The main issues are:

  • Consumers can’t tell exporter objects when they are done with the shared memory, so the exporters can release the memory safely.
  • There is no good way to work with discontiguous memory.
  • There is no way to describe the internal structure of the memory with the 2.x buffer protocol. The consumer must “know” or the exporter and consumer must share some scheme to put such metadata in the buffer.

PEP-3118 is motivated by NumPy and PIL. These libraries are not part of the Python standard libraries, but they are definitely standards in the Python scientific community (NumPy) and image processing community (PIL). These libraries use discontiguous memory extensively and their needs informed the PEP. NumPy deals primarily with N-dimensional arrays, and uses a strided memory model to enable efficient slicing. PIL deals primarily with images that are often stored in an array of pointers to contiguous memory buffers.

The new buffer protocol addresses all these issues:

  • It gets rid of some little-used features, such as char-buffer and multiple segment sequences.
  • It adds a notification function for consumers to call when they are done with the memory.
  • It adds a variable that describes the structure of the memory (a-la struct).
  • It also adds shape and stride information (for NumPy).
  • There is a mechanism for sharing arrays that must be accessed using pointer indirection (PIL).
  • It provides functions for copying contiguous data in and out of object supporting the buffer interface.

The bottom line result of the changes is that low-level libraries that need to manipulate raw memory can do it in a very efficient manner and interoperate with other libraries without unnecessary conversions or putting private metadata info inside the buffers. High-level applications that use these libraries should benefit from improved performance with no code changes.

PEP-3106: Revamping dict.keys(), .values() and .items()

The dictionary (dict) is arguably the most useful and optimized data structure in Python. In Python 2.5 the .keys(), .values() and .items() methods return lists that contain the keys, values and pairs of key-values respectively. Usually, developers calling these methods intend to iterate over the result: they don’t really need a list. The list return value is expensive because the keys/values/items must be read from the dictionary and copied into the list. The Python 2.5 dict also supports.iterkeys(), .itervalues() and .iteritems() methods that just return an iterator:

>>> d.iterkeys()>>> d.itervalues()>>> d.iteritems()

However, many developers end up using the more natural and shorter names exposed by the heavyweight methods (keys(), values() and items()).

Python 3.0 changes keys(), values() and items() so they return a lightweight set-like object, effectively making them behave like the old iter* methods, and removed the iter* methods themselves. In Python 3.0:

>>> d = {}>>> d.keys()>>> d.values()>>> d.items()

It is important to realize that all these iterators are completely unordered (not even necessarily in insertion order), but the keys, values and items are synchronized:

>>> d = dict(a=1, b=2, c=3)>>> for k in d:...   print(k)... acb>>> for v in d.values():...   print(v)... 123

If you do need a list you can simply wrap a list around the result of keys(), values() and items():

>>> d = dict(a=1, b=2, c=3)>>> list(d.keys())['a', 'c', 'b']>>> list(d.values())[1, 3, 2]>>> list(d.items())[('a', 1), ('c', 3), ('b', 2)]

Here’s a little experiment that explores the difference between keys() and iterkeys(). I populated a dictionary with five million entries, and then wrote a little function that—based on an argument (use_iter)—iterates over all the keys in the dictionary using either keys() or iterkeys(), and measures the elapsed time. I then ran the function five times each for both iterkeys() and keys(), and took the average. Here’s the code:

# Iterate over all the keys using either keys() or iterkeys()def f(use_iter):  func = d.iterkeys if use_iter else d.keys  s = time.time()  for k in func():    pass  return time.time() - s# Run the test function 5 times with iterkeys()total = 0for i in range(5):  t = f(use_iter=True)  print t  total += tprint 'Average time for iterkeys():', total / 5# Run the test function 5 times with keys()total = 0for i in range(5):  t = f(use_iter=False)  print t  total += tprint 'Average time for keys():', total / 5

The results showed that iterkeys() takes about 60% of the time it takes keys():

0.2915849685670.2983300685880.3105630874630.3195681571960.361966133118Average time for iterkeys(): 0.3164024829860.5377128124240.5258438587190.5355801582340.5690560340880.535542964935Average time for keys(): 0.54074716568

That may sound like a big difference; however, think about it from a different perspective. If you need to iterate over five million dict entries, all you gain by using iterkeys() rather than keys() is 0.2 seconds. Given that any real-world code would do something with these five million items that probably requires some time, a two-tenths of a second gain doesn’t seem all that impressive. To illustrate, I modified the function f so it simply sums up all the keys, which is a very cheap operation:

def f(use_iter):  func = d.iterkeys if use_iter else d.keys  s = time.time()  sum = 0  for k in func():    sum += k  return time.time() - s

This time, the average for iterkeys() was: 2.0865404129 and for keys(): 2.11347484589. So, the iterkeys() iteration now takes 98.7% of the duration of the keys() iteration. If you do some real work inside the loop, such as string manipulation, serious computation, or some IO, there is virtually no difference between the two.

I ran the same tests on Python 3.0 for the keys() method and it performed pretty much like the Python 2.5 iterkeys() method (maybe even a little faster if you throw away the extreme 0.54 result):

0.3087248802190.2723491191860.2582690715790.2594258785250.540471076965Average time for keys(): 0.327848005295

Finally, I ran some more tests just on lookup. The idea is that if you want to determine whether a dict contains a certain value, you don’t need to create a list of all the values and search it, you can just use the in set operation, which is very efficient. If you compare value lookups on Python 2.5 to value lookups on Python 3.0 you get amazing performance differences due to the required copy of all the values for each lookup. Here’s a test function that performs 100 lookups on the now-familiar five million item dict:

def g():  s = time.time()  for i in range(100):    y = i in d.values()  return time.time() - stotal = 0for i in range(5):  t = g()  print(t)  total += tprint('Average time for 100 lookups:', total / 5)

Python 2.5 averaged about 20 seconds, while Python 3.0 averaged less than a millisecond. This sounds fantastic until you try the lookup in Python 2.5 using itervalues()—and you get the same numbers as Python 3.0.

The conclusion is that this is a nice syntactic cleanup, but don’t expect your code to become blazingly fast just because the common dict methods are now set-like iterators. That said, if you have code that performs many lookups on the keys or values of large dictionaries and you’re using keys() and values(), you would be wise to rewrite the lookups using iterkeys() or itervalues().

Platform-Specific Changes (Windows, Mac OSX)

There were several interesting changes for Windows. Python 3.0 requires at minimum Windows 2000 Service Pack 4. Windows 95, 98, ME and NT 4 are no longer supported. In addition, the default compiler is now Visual Studio 2008 (Microsoft provides a free Express version). This is important if you build extension modules on Windows, because extension modules must be built using the same compiler that built the Python interpreter itself. Other interesting additions for writing cross-platform system administration are the functions os.path.expandvars() and os.path.expanduser(). These functions can use the tilda (~) as shorthand for the user’s home directory and can access environment variables in the Windows %VAR% format:

>>> os.path.expanduser('~')'c:\Documents and Settings\Gigi'>>> os.path.exapnsvars('%USERNAME%')Gigi

The Mac OS X port mostly eliminated old modules.

The next and final article in this series covers Python 2.6 and porting code from Python 2.x to 3.0.


About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist