skip to content

six — Python 2/3 Compatibility (Mostly Legacy in 2026)

Package-level reference for six on PyPI — what it did, why it still appears in dependency trees, and why new code should avoid it.

9 min read 13 snippets deep dive

six#

What it is#

six is a single-file utility library that papered over the differences between Python 2 and Python 3 from roughly 2010 through Python 2’s end-of-life in 2020. It exposed renamed builtins (six.moves.urllib, six.string_types, six.iteritems), portable metaclass syntax (six.with_metaclass), and small compatibility shims (six.PY2, six.PY3, six.text_type). The library’s name comes from “2 × 3” — bridging the two Python lines.

In 2026 six is mostly legacy. Python 2 is six years past EOL; new code should target Python 3.8+ directly and use no six constructs at all. The package still appears in many dependency trees because long-lived libraries (notably python-dateutil, several Django plugins, OpenStack components, AWS-internal packages) haven’t migrated. Reach for six only when modifying code that already depends on it.

Install#

pip install six

Output: (none — exits 0 on success; pure-Python, zero dependencies)

uv add six

Output: dependency resolved + added to pyproject.toml

poetry add six

Output: updated lockfile + virtualenv install

You almost never install six directly — it arrives transitively. If you find yourself adding it to a new project, stop and reconsider.

Versioning & Python support#

  • Current version is 1.16.0 (released 2021). The library is in maintenance mode; no further feature releases are planned.
  • Supports Python 2.7 and Python 3.4+. Effectively all modern Python 3 versions work.
  • 1.17.x may ship to address security or packaging issues, but the API is frozen.

Package metadata#

Optional dependencies & extras#

  • None. six is intentionally a single-file pure-Python module.

Alternatives#

PackageTrade-off
Native Python 3 idiomsThe right answer in 2026. dict.items() instead of six.iteritems(d); urllib.parse instead of six.moves.urllib.parse; metaclass=… directly instead of six.with_metaclass.
futureLarger compatibility shim — explicit Python-2-from-Python-3 backports. Even more legacy than six.
python-modernizeA CLI to mechanically rewrite Py2 code into Py2/3 compatible code using six. Useful only when migrating away from Py2.
2to3 (stdlib, removed in 3.13)Mechanical converter; produces Py3-only output. Use when you no longer need Py2 support.

Common gotchas#

  1. six.moves is lazyfrom six.moves import urllib works, but from six.moves.urllib import parse may not on some setups (depends on import-hook timing).
  2. six.string_types is (str,) on Py3, (str, unicode) on Py2. New Py3 code should just check isinstance(x, str).
  3. six.text_type is str on Py3, unicode on Py2. In Py3-only code, you can drop it entirely.
  4. six.b("...") was the way to write bytes literals on both Py2 and Py3. Py3 supports b"..." natively — use that.
  5. six.iteritems(d) is d.iteritems() on Py2, iter(d.items()) on Py3. In Py3-only code, just write d.items().
  6. six.with_metaclass(M, B) was the portable metaclass spell. Py3 syntax class C(B, metaclass=M) is native — use it.
  7. Dependency conflicts. A newer library says six is unused (it removed the dep), but python-dateutil still pulls it in. Don’t try to remove six from requirements.txt directly — it appears transitively.

Real-world recipes#

Modern Python 3 — drop six entirely#

These are the patterns to prefer in new code; six is shown only for comparison.

Recipe 1 — Dict iteration.

# six (legacy)
from six import iteritems
for k, v in iteritems(my_dict):
    print(k, v)

# Modern Python 3
for k, v in my_dict.items():
    print(k, v)

Output: identical iteration order and behavior. In Py3, dict.items() returns a view, not a list — usually what you want.

Recipe 2 — String types.

# six (legacy)
import six
if isinstance(x, six.string_types):
    ...

# Modern Python 3
if isinstance(x, str):
    ...

Output: Py3 has one string type. Use it directly.

Recipe 3 — Metaclass syntax.

# six (legacy)
import six
class MyClass(six.with_metaclass(MyMeta, BaseClass)):
    ...

# Modern Python 3
class MyClass(BaseClass, metaclass=MyMeta):
    ...

Output: native syntax is cleaner and the type checker understands it correctly.

Recipe 4 — Renamed module imports.

# six (legacy)
from six.moves.urllib.parse import urlparse
from six.moves import range, queue, cPickle as pickle

# Modern Python 3
from urllib.parse import urlparse
import queue
import pickle

Output: stdlib paths are stable; six.moves only confuses IDEs and type checkers.

Patterns still seen in 2026#

The patterns you might still encounter when reading old code:

Recipe 5 — six.PY2/six.PY3 branching.

import six
if six.PY2:
    # legacy Py2-only branch (effectively dead code in 2026)
    raise RuntimeError("Python 2 not supported")

Output: since Py2 is EOL, the six.PY2 branch is dead code. Modernization checklist: delete those branches outright.

Recipe 6 — six.b() for byte literals.

import six
data = six.b("hello")     # b'hello' on both Py2 and Py3

# Modern: just write b"hello"
data = b"hello"

Output: identical bytes; the modern form removes the dependency.

Recipe 7 — Bridging code that hasn’t migrated yet.

# If you must touch legacy Py2/3 code that still uses six, keep using six
# until the next refactor lets you remove it. Don't mix idioms within one module.
import six
for k, v in six.iteritems(d):
    ...

Output: consistent style within a single legacy module reduces review friction; full removal is a separate task.

Performance tuning#

  • six is fast. Each shim is a single attribute lookup. No measurable perf overhead in any normal use.
  • Removing six from your code is the only “tuning” — and the gain is import-time and clarity, not runtime.
  • six.moves lazy imports can cause first-use latency in cold-start environments (Lambda). Lazy-import dateutil etc. instead.

Version migration guide#

The only meaningful migration is away from six, not between six versions. A typical sequence:

  1. Identify all six imports: grep -r "import six\|from six" src/
  2. Replace each per the patterns above (or use pyupgrade --py3-plus / ruff --select UP).
  3. Remove six from requirements.txt / pyproject.toml (if it was a direct dep).
  4. Run tests on the lowest Python version you still support.
  5. If six still shows in pip freeze, it’s transitive — wait for the upstream library to drop it.
# Before
import six
print(six.PY3, six.iteritems({"a": 1}), six.with_metaclass)

# After (Py3-only)
print(True, {"a": 1}.items(), "use metaclass= keyword")

Output: functionally equivalent; one fewer dependency.

Tooling that helps:

  • pyupgrade --py3-plus — mechanically removes most six usage.
  • ruff --select UPUP rules flag six.iteritems, six.string_types, and friends.
  • python-modernize (older) — produces Py2/3-compat code; useless if you’re Py3-only.

Security considerations#

  • six itself has no known CVEs. It’s pure Python with no I/O, no parsing of untrusted input.
  • Pin a recent version (six>=1.16) — older versions had minor packaging issues but no security ones.
  • The real security concern is unmaintained code that depends on six — if a library hasn’t dropped six by 2026, ask whether it’s getting other security updates.

Testing & CI#

There’s almost nothing to test in six itself. The useful CI check is “do we have any six usages we missed”:

# Fail CI if any new code imports six
git diff main HEAD -- 'src/**/*.py' | grep -E '^\+.*\b(import six|from six)' && exit 1 || true

Output: exits 1 if a PR added a new six import; useful as a pre-merge gate.

# Inventory transitive six usage
pip show six | grep "Required-by"

Output: lists which installed packages still pull in six (e.g. python-dateutil, some Django plugins).

Ecosystem integrations#

six is a dependency of, not an integrator with — the question is “which packages still pull it in”:

  • python-dateutil — uses it on PyPy and edge platforms; remains a transitive source.
  • Older Django plugins — many were last updated pre-EOL and still depend on six.
  • OpenStack components — historically heavy six users; many still do.
  • urllib3 1.26.x — DROPPED six in 2.x. The 1.26 line still pulls it.
  • tensorflow, pandas, boto3 — all dropped six by 2022-2023.

Compatibility matrix#

PythonsixNotes
2.71.16Final useful target.
3.41.16Stable.
3.5-3.71.16Stable.
3.81.16Stable.
3.91.16Stable.
3.101.16Stable.
3.111.16Stable.
3.121.16Some collections imports warn; pinned 1.16.0 includes the fix.
3.131.16Continues to work; effectively no benefit.

Production deployment#

  • Don’t actively install six. Treat it as a smell whenever it appears as a direct dependency.
  • Tolerate it as a transitive. pip show six listing python-dateutil as a dependent is fine; just don’t write new six code.
  • Modernization audits. Once a year, run ruff --select UP (or pyupgrade --py3-plus) and remove any remaining six usage you control.
  • Track upstream drops. When python-dateutil finally drops six (announced but not yet released as of 2026), your installs lose the transitive dep automatically.

When NOT to use this#

  • New code, new project, anything modern. Don’t.
  • You support only Python 3.8+. Don’t.
  • You’re tempted to “future-proof” against a Py2 comeback. That’s not happening.

Reach for six only when:

  • Maintaining existing legacy code where consistent style matters.
  • A library you must use still imports six symbols and you’re patching it locally.

Troubleshooting common errors#

Error / SymptomLikely causeFix
ModuleNotFoundError: No module named 'six.moves.urllib'Lazy import not registeredRestart the interpreter; verify six is the imported module, not a stale .pyc.
AttributeError: module 'six' has no attribute 'PY3'Importing a six.py from your own projectRename your local file.
DeprecationWarning from six on Python 3.12+Old six versionUpgrade to six>=1.16.0.
Tools flag six.iteritems callsLinter rules (UP) firingReplace with .items() — the linter is right.
pip install -U six “doesn’t help”six is already at max versionThe library is in maintenance mode.

See also#