Using the module API

get_mac_address() covers most uses of getmac. This page covers the internal methods used by getmac to resolve MAC addresses on various platforms.

Internally, getmac picks a method for each kind of lookup. The method is a class that knows a particular way of finding a MAC address, such as reading /sys/class/net/<iface>/address or running ip neighbor show. This page explains how those methods work, and how to implement a new method.

Warning

Most of what this page describes lives in getmac.getmac and is an internal API that may change between releases. Use it with care.

Where things live

Method isn’t exported from the top-level package, so import it with from getmac.getmac import Method. Every built-in method and its docstring is listed in the API reference.

Methods 101

Methods

A method is a subclass of Method and should set 3 attributes:

  • platforms: a set with the platforms it works on, such as {"linux", "darwin"}.

  • method_type: the kind of lookup it does (see below).

  • network_request: whether using it sends traffic on the network.

A method overrides two functions:

  • test(): a cheap check that the method can work on this system, such as “is the ip command installed?”

  • get(): performs the lookup. It takes one argument, and returns the result or None. The argument’s meaning depends on the method, e.g. for an interface method, it would be the interface name (e.g. "eth0").

Method types

method_type

Used for

get() argument

get() returns

ip4

get_mac_address(ip=...), get_mac_address(hostname=...), and get_mac_address() with no arguments on Windows (see the note below)

IPv4 address

MAC address

ip6

get_mac_address(ip6=...)

IPv6 address

MAC address

ip

Both ip4 and ip6 lookups

IPv4 or IPv6 address

MAC address

iface

get_mac_address(interface=...), and get_mac_address() with no arguments

Interface name

MAC address

default_iface

get_default_interface(), and get_mac_address() with no arguments

Empty string

Interface name

Note

get_mac_address() with no arguments does a default_iface lookup, then an iface lookup of the interface it finds. If the default interface can’t be found or it doesn’t have a MAC, the first interface from socket.if_nameindex() that has a MAC and isn’t a loopback interface is used instead, except on Windows.

On Windows with network_request=True (the default), it first gets the IP address of the interface with the default route from fetch_ip_using_dns(), and does an ip4 lookup of that address. The default_iface lookup is only done if that doesn’t find a MAC.

Note

There is one cache per lookup type: ip4, ip6, iface and default_iface.

Platforms

The platform is a lowercase identifier, consts.PLATFORM, unless you override it with settings.OVERRIDE_PLATFORM. To see what your system is detected as:

from getmac.variables import consts

print(consts.PLATFORM)  # For example, "linux"

The identifiers used by the built-in methods are listed in Method.VALID_PLATFORM_NAMES.

The platform "other" is used for methods that are generic enough to try on unknown systems, and are only used as a last resort if the current platform matches no method.

Choosing a method

The first time a lookup is performed for a particular type (e.g. ip4), the cache for that type is initialized by initialize_method_cache():

  1. Selects methods in METHODS with a matching method_type.

  2. Selects methods whose platforms set includes the current platform, or "other" if nothing matches (see the section above).

  3. If network_request=False, it excludes methods that have network_request=True.

  4. Creates instances of the selected methods and calls their test(). The first method that passes (returns True) becomes the primary method, stored in METHOD_CACHE[type]. The others that pass become fallbacks, stored in order in FALLBACK_CACHE[type]. Methods whose test() returns False or raises an exception are left out.

initialize_method_cache() will raise RuntimeError if:

  • No method has the requested type.

  • No method suits the platform, even after trying "other".

  • Every candidate method fails its test().

Note

The cache is built lazily on first lookup and reused for the rest of the process’s life. Methods aren’t tested again, so later changes to METHODS, to OVERRIDE_PLATFORM, or to the system itself (such as installing a command) don’t affect a type whose cache is already built.

Falling back during a lookup

Each lookup calls get() on the primary method from the cache and:

  • If get() returns a value, winner winner chicken dinner, that’s the result.

  • If get() returns None, the result is None. The fallbacks are not tried. None means “not found”, for example “there’s no such interface”, not “this method doesn’t work”.

  • If get() raises an exception, or sets self.unusable = True and returns None, the method is removed from the cache. The first fallback becomes the primary method, and the lookup is retried with it using the same argument. The exception is logged as a warning, not raised.

  • A CalledProcessError with exit code 1 is treated like returning None, because many commands exit with 1 when the interface or host doesn’t exist. Any other exit code counts as a failure and leads to a fallback.

  • With network_request=False, methods that send network requests (their network_request attribute is True) are skipped, both for the lookup and as fallbacks. They stay in the cache, since the cache may have been built by a lookup that allowed them.

IPv4 lookups and network requests

When get_mac_address() looks up an IPv4 address (ip= or hostname=) with network_request=True (the default), it first tries to make sure the host is in the system’s ARP table:

  1. If ArpFile is in the ip4 cache, it’s tried first, since reading /proc/net/arp is fast. If it fails, it’s removed from the cache, but no other method is tried yet.

  2. Otherwise, if CtypesHost or ArpingHost is in the ip4 cache, it’s made the primary method, since it sends an ARP request itself.

  3. If neither applies, an empty UDP packet is sent to the host on the port specified by settings.PORT to force the system to populate the ARP table.

Then the host is looked up with the ip4 cache. If the UDP packet was sent and the host isn’t found, it’s looked up again until it’s found or settings.ARP_TIMEOUT seconds have passed (by default, it isn’t looked up again). IPv6 lookups send the UDP packet and use ARP_TIMEOUT the same way, but there’s no ArpFile or ARP request step.

Seeing which methods are used

Inspecting the caches

After a lookup, the caches show which methods were chosen. Converting a method instance to a string with str gives its class name.

from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE

# Force caches to be populated
get_mac_address()

# Print the name of each method in the cache
for method_type, method in METHOD_CACHE.items():
    fallbacks = [str(m) for m in FALLBACK_CACHE[method_type]]
    print(f"{method_type}: {method!s}, fallbacks: {fallbacks}")

Example output on a Linux system:

ip4: None, fallbacks: []
ip6: None, fallbacks: []
iface: SysIfaceFile, fallbacks: ['FcntlIface', 'IpLinkIface']
default_iface: DefaultIfaceLinuxRouteFile, fallbacks: ['DefaultIfaceIpRoute']

Two helper functions look methods up by name:

  • get_instance_from_cache() returns the cached instance of a method, primary or fallback, for one lookup type. The name must match the class name exactly, for example get_instance_from_cache("iface", "SysIfaceFile").

  • get_method_by_name() returns the class with a given name from METHODS, ignoring case. This is how FORCE_METHOD finds a method.

Logging and debug output

getmac logs to the getmac logger with Python’s logging module. It attaches a NullHandler to that logger, so nothing is shown until logging is configured. settings.DEBUG controls how much detail is logged:

  • 0 (the default): warnings, errors, and a few debug messages, such as when a cache is built and the raw MAC that was found.

  • 1: failed tests, the contents of the caches, each get() attempt, and how long the lookup took.

  • 2: the methods left after filtering by type, by platform, and by test().

  • 3: every command that’s run.

  • 4: the output of every command, and the full METHODS list.

import logging

from getmac import get_mac_address, settings

logging.basicConfig(format="%(levelname)-8s %(message)s", level=logging.DEBUG)
settings.DEBUG = 2

get_mac_address(interface="lo")

Example output on Linux, shortened:

DEBUG    Initializing 'iface' method cache (platform: 'linux')
DEBUG    12 type-filtered methods for 'iface': SysIfaceFile, FcntlIface, LanscanIface, ...
DEBUG    6 platform-filtered methods for 'linux' (method_type='iface'): SysIfaceFile, FcntlIface, IfconfigWithIfaceArg, IfconfigOther, IpLinkIface, NetstatIface
DEBUG    Test failed for method 'IfconfigWithIfaceArg'
DEBUG    Test failed for method 'IfconfigOther'
DEBUG    Test failed for method 'NetstatIface'
DEBUG    3 tested methods for 'iface': SysIfaceFile, FcntlIface, IpLinkIface
...
DEBUG    Attempting get() (method='SysIfaceFile', method_type='iface', arg='lo')
DEBUG    Raw MAC found: 00:00:00:00:00:00

The command-line equivalent is getmac -v -dd -i lo, see Command-Line Interface.

Calling lookups directly

get_by_method()

get_by_method() does one lookup of a given type (e.g., "ip4") using the caches and fallbacks described above.

It returns whatever the method’s get() returned, without the clean-up that get_mac_address() does. Many built-in methods return the MAC the way the command printed it, so pass the result through clean_mac() to get a lowercase, colon-separated MAC (aka what is normally returned from get_mac_address()).

from getmac import utils
from getmac.getmac import get_by_method

raw = get_by_method("iface", "lo")
print(repr(raw))
print(utils.clean_mac(raw))

Example output on Linux:

'00:00:00:00:00:00\n'
00:00:00:00:00:00

A few more things to know:

  • An empty argument returns None and logs an error, except for default_iface type methods, which take no argument.

  • It raises RuntimeError when no method can be used, as described in Choosing a method.

  • Its network_request argument has the same effect as in initialize_method_cache(), and only applies if that type’s cache hasn’t been built yet.

get_default_interface()

get_default_interface() is get_by_method("default_iface"), so it can also raise RuntimeError. It doesn’t change the default interface that get_mac_address() remembers between calls (gvars.DEFAULT_IFACE).

from getmac import get_default_interface

try:
    print(get_default_interface())
except RuntimeError as err:
    print(err)

Settings

The settings are attributes of getmac.settings, an instance of Settings. Configuration has a short overview, and this section adds how each one interacts with method selection.

FORCE_METHOD

The name of a method to use for every lookup, e.g. "IpLinkIface". The name is looked up in METHODS, ignoring case. If no method matches, the lookup logs an error and returns None. On the command line, use --force-method.

  • A new instance is created for each lookup, and test() is never called.

  • platforms, method_type and network_request are ignored, and the caches aren’t used or changed.

  • There’s no fallback, and exceptions raised by the method are not caught.

  • It applies to every lookup type, including get_mac_address() called with no arguments.

  • For an IPv4 lookup with network_request=True, get_mac_address() still builds the ip4 cache and sends the UDP packet described in IPv4 lookups and network requests before calling the forced method. The UDP packet isn’t sent if the forced method is "CtypesHost" or "ArpingHost" and it’s in the ip4 cache, since it sends an ARP request itself.

  • One case differs: forcing "ArpFile" for an IPv4 lookup with network_request=True. The ArpFile instance in the ip4 cache is then tried first, as described in IPv4 lookups and network requests, and the points above don’t apply to that attempt: it uses the cache, exceptions are caught, and a failure removes it from the cache. If that attempt finds a MAC, it’s returned, no UDP packet is sent, and the forced method isn’t called. Otherwise the UDP packet is sent and a new ArpFile instance runs as described above.

OVERRIDE_PLATFORM

Override the detected platform identifier to whatever you set, e.g. "linux". Use --override-platform on the command line. It’s only read when a cache is built, so set it before the first lookup or reset the caches afterwards. It only changes which methods are chosen. Other platform-specific behavior in get_mac_address(), such as how the default interface is found on Windows, still follows the detected platform.

PORT

The UDP port used to populate the ARP table, as described in IPv4 lookups and network requests.

ARP_TIMEOUT

How long to keep looking up a host after sending the UDP packet, as described in IPv4 lookups and network requests. Use --arp-timeout on the command line.

COMMAND_TIMEOUT

How long a command can run before it’s stopped. A method whose command is stopped is treated like one that raised an exception: it’s marked unusable and removed from the caches, and the next method is tried.

DEBUG

How much detail is logged, see Logging and debug output.

Adding a custom method

You can add your own methods at runtime, for example to support a command getmac doesn’t know about. If the method would be useful to others, consider contributing it to getmac, see Adding a new method.

Writing the class

The examples below use this method, which always returns the same MAC. Save it as my_methods.py.

my_methods.py
from typing import Optional

from getmac.getmac import Method


class FixedMacIface(Method):
    """Always returns the same MAC address. Only useful as an example."""

    platforms = {"linux", "darwin", "windows"}
    method_type = "iface"

    def test(self) -> bool:
        return True

    def get(self, arg: str) -> Optional[str]:
        return "02:00:00:00:00:01"

Requirements to follow for implementing a method:

  • platforms must include each platform the method should run on, see Platforms.

  • method_type must be one of the values in Method types.

  • Set network_request = True if get() can result in network traffic (e.g. an ARP request).

  • test() should be cheap, such as checking that a command or file exists. check_command() and check_path() do this. If it raises, getmac treats it as a failed test.

  • get() should return the MAC as a str, or return None if it wasn’t found.

  • When get() can’t work on this system, it should raise an exception or set self.unusable = True, so that getmac moves on to the next method. See Falling back during a lookup.

  • A default_iface method returns an interface name, and its get() is called with an empty string, so define it as def get(self, arg: str = "").

getmac.utils has helpers for running commands (popen()), reading files (read_file()) and matching output (search()), and Constants has regular expressions for MAC addresses, such as MAC_RE_COLON.

Here’s a more realistic method. It parses the output of ip -brief link show dev <iface>, which looks like lo  UNKNOWN  00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>.

from typing import Optional

from getmac import utils
from getmac.getmac import Method
from getmac.variables import consts


class IpBriefLinkIface(Method):
    """Get the MAC of an interface from ``ip -brief link show dev <iface>``."""

    platforms = {"linux"}
    method_type = "iface"

    def test(self) -> bool:
        return utils.check_command("ip")

    def get(self, arg: str) -> Optional[str]:
        # If the interface doesn't exist, "ip" exits with code 1, which getmac
        # treats as "not found". Other exit codes make getmac try the next method.
        output = utils.popen("ip", f"-brief link show dev {arg}")
        return utils.search(r"^\S+\s+\S+\s+" + consts.MAC_RE_COLON, output)

Registering it

Add the class to METHODS before the first lookup of its type (e.g. "iface"). The order of METHODS is the order methods are tested in, so insert it at the front to make it the primary method:

from getmac import get_mac_address
from getmac.getmac import METHODS

from my_methods import FixedMacIface

METHODS.insert(0, FixedMacIface)  # Before the first "iface" lookup

print(get_mac_address(interface="eth0"))

Output:

02:00:00:00:00:01

If you append it instead, any built-in method of the same type that passes its test comes first, and yours becomes a fallback.

Change the list in place, as above, or replace the module attribute (getmac.getmac.METHODS = [...]).

If lookups already happened

Adding a method doesn’t affect a type whose cache is already built. To make getmac choose again, set that type’s entry in METHOD_CACHE to None and empty its FALLBACK_CACHE list. The next lookup of that type tests METHODS again.

from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE, METHODS

from my_methods import FixedMacIface

print(get_mac_address(interface="lo"))  # Builds the "iface" cache

METHODS.insert(0, FixedMacIface)
print(get_mac_address(interface="lo"))  # Still uses the method chosen earlier

METHOD_CACHE["iface"] = None  # Forget the "iface" selection
FALLBACK_CACHE["iface"] = []
print(get_mac_address(interface="lo"))  # METHODS is tested again

Example output on Linux:

00:00:00:00:00:00
00:00:00:00:00:00
02:00:00:00:00:01

To start over completely, reset every type. Also clear gvars.DEFAULT_IFACE, where get_mac_address() remembers the default interface after the first call without arguments. Without that, a new default_iface method is never used by get_mac_address().

from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE
from getmac.variables import gvars


def reset_method_caches() -> None:
    """Forget every method getmac has chosen, and the cached default interface."""
    for method_type in METHOD_CACHE:
        METHOD_CACHE[method_type] = None
        FALLBACK_CACHE[method_type] = []
    gvars.DEFAULT_IFACE = ""

check_command() also caches whether each command exists, in gvars.CHECK_COMMAND_CACHE. Clear that dict as well if commands were installed or removed since the first lookup.

To undo these changes in tests, see Undoing changes in tests.

Using a method without registering it

You can also put an instance straight into METHOD_CACHE. This skips METHODS, test() and the platform check. Fallbacks that are already cached are kept, but if that type’s cache hadn’t been built yet, there are none.

from getmac import get_mac_address
from getmac.getmac import METHOD_CACHE

from my_methods import FixedMacIface

METHOD_CACHE["iface"] = FixedMacIface()

print(get_mac_address(interface="eth0"))  # 02:00:00:00:00:01

The instance stays the primary method until it fails and getmac falls back.

Forcing a custom method

FORCE_METHOD only finds methods that are in METHODS, so register the method first. Where it is in the list doesn’t matter.

from getmac import get_mac_address, settings
from getmac.getmac import METHODS

from my_methods import FixedMacIface

METHODS.append(FixedMacIface)
settings.FORCE_METHOD = "FixedMacIface"

print(get_mac_address(interface="eth0"))  # 02:00:00:00:00:01

Removing a built-in method

To stop getmac from using a built-in method, for example one that misbehaves on your system, remove it from METHODS. As with adding a method, reset the type’s cache if a lookup of that type already happened.

from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE, METHODS, SysIfaceFile

METHODS.remove(SysIfaceFile)
METHOD_CACHE["iface"] = None  # Only needed if an "iface" lookup already happened
FALLBACK_CACHE["iface"] = []

get_mac_address(interface="lo")
print(METHOD_CACHE["iface"], [str(m) for m in FALLBACK_CACHE["iface"]])

Example output on Linux:

FcntlIface ['IpLinkIface']

Undoing changes in tests

Changes to METHODS, METHOD_CACHE, FALLBACK_CACHE and settings affect the whole process, so tests that make them should undo them afterwards. With pytest, the built-in monkeypatch fixture can give each test its own copy of the list and caches, and puts the originals back when the test ends:

import getmac.getmac as gm
import pytest
from getmac import get_mac_address
from getmac.variables import gvars

from my_methods import FixedMacIface


@pytest.fixture
def isolated_getmac(monkeypatch):
    """Give the test its own METHODS list and empty caches."""
    monkeypatch.setattr(gm, "METHODS", [FixedMacIface, *gm.METHODS])
    monkeypatch.setattr(gm, "METHOD_CACHE", dict.fromkeys(gm.METHOD_CACHE))
    monkeypatch.setattr(gm, "FALLBACK_CACHE", {key: [] for key in gm.FALLBACK_CACHE})
    monkeypatch.setattr(gvars, "DEFAULT_IFACE", "")


def test_fixed_mac_is_used(isolated_getmac):
    assert get_mac_address(interface="eth0") == "02:00:00:00:00:01"

Use monkeypatch.setattr(settings, "FORCE_METHOD", ...) in the same way to change a setting for one test.