Architecture¶
This page explains how getmac’s code is organized, how a lookup flows through it, and a few of the design decisions behind it. It’s for contributors, and for anyone who wants to know what getmac does on their system before using it. For how methods work in detail, see How methods are chosen and used, and for how to add one, see Adding a new method.
Goals and constraints¶
getmac has one job: return the MAC address of a local network interface, or of a host on the local network, on as many platforms as possible, with nothing to install. That leads to a few constraints the design follows:
Pure Python, standard library only. No runtime dependencies and no C-extensions, so getmac can be vendored or installed anywhere Python runs (CPython and PyPy, 3.9 and newer). Instead of calling platform APIs, it gets MACs the way a sysadmin would: by running commands like
arp,ip,ifconfigandipconfig.exe, reading files like/proc/net/arp, and parsing the output with regular expressions. A few methods use standard library features likefcntlandctypes.Many platforms, one interface. Windows, Linux, macOS, WSL, the BSDs, Solaris, HP-UX, Android, Docker, and BusyBox all have different commands and files for the same information. The caller never sees the difference.
No privileges needed. Everything getmac does works as a normal user, with one exception: some methods, such as
arping, need root. They’re tried when they’re installed, and if one is installed but doesn’t have the privileges it needs, that lookup returnsNone(see Do I need root or admin?).Fail quietly, by default. A lookup that can’t find a MAC returns
Noneand logs why. getmac only raises when something is wrong with how it was called, or when no method at all works on the platform (see Errors and exceptions).
This replaced the pre-0.9.0 approach of throwing every method at the wall until one stuck, which was slow (a miss could take seconds), prone to false positives, and hard to test. The rewrite summary from the 0.9.0 release has the full story.
Where things live¶
getmac is a small package with a handful of modules:
getmac(__init__.py): the public API.get_mac_address(),get_default_interface(),settings, and__version__are the names in__all__. Nothing else is imported into the top-level package.getmac.getmac: theMethodbase class, every built-in method, theMETHODSlist, theMETHOD_CACHEandFALLBACK_CACHEdicts, and lower-level functions such asget_by_method()andinitialize_method_cache().getmac.variables: thesettings,constsandgvarsobjects (see below), documented as theSettings,ConstantsandVariablesclasses.getmac.utils: helper functions used by methods, such aspopen()for running commands,search()for matching their output, andclean_mac()for normalizing a MAC.getmac.__main__: the command line interface (Command-Line Interface), a thinargparsewrapper aroundget_mac_address().
Method isn’t exported from the top-level package, so import it with from getmac.getmac import Method.
How a lookup works¶
Normalize the arguments.
get_mac_address()accepts one ofinterface,ip,ip6, orhostname.bytesare decoded as UTF-8, andipaddressobjects are converted to strings (an IPv6 object passed asipis treated asip6, and a network raisesValueError).localhostand127.0.0.1return00:00:00:00:00:00without doing anything else. A hostname is resolved to an IPv4 address with the system resolver.Refresh the ARP/NDP table, if asked. Operating systems only know the MAC of a host they’ve talked to recently. For a remote host, when
network_requestis true (the default), getmac sends something to get the host into the table. For IPv4, on Linux it reads the ARP table file (ArpFile) first, and if the host is there, nothing is sent. If it isn’t, getmac uses a method that makes a real ARP request (CtypesHoston Windows, orArpingHost) if one is available, and only then falls back to sending one empty UDP packet to the host onsettings.PORT(55555 by default), which makes the OS resolve the host and add it to its table. IPv6 goes straight to the UDP packet.ARP_TIMEOUTcontrols how long getmac keeps checking the table afterwards. Withnetwork_request=False, getmac sends nothing and only reads what’s already in the table. IPv4 lookups and network requests has the exact sequence.Pick a method. Each way of getting a MAC is a
Methodsubclass with amethod_type(ip4,ip6,ipfor both,iface, ordefault_iface), theplatformsit supports, and whether it sends network traffic.initialize_method_cache()filters theMETHODSregistry by type and by the detected platform (orOVERRIDE_PLATFORM), creates each candidate, and calls itstest(), which is a cheap check such as “does this command exist” or “can this file be read”. The first method that passes is cached for that type, and the others are kept as fallbacks. This happens once per method type per process, which is why the first call is slower than the rest.Run it, and fall back if it fails.
get_by_method()calls the cached method’sget(arg). Ifget()raises, or the method marks itself unusable, getmac logs the problem, drops the method, and tries the next fallback. If the last fallback fails too, the lookup returnsNone. ARuntimeErrorcomes from an earlier point:initialize_method_cache()raises it when no method matches the type and platform, or when none of the matching methods passtest(). That means the platform isn’t supported, or something changed underneath getmac (see Errors and exceptions).Clean the result. Whatever the method found (a line of command output, the contents of a file, or a struct from a system call) goes through
clean_mac(), which returns a lowercase colon-separated MAC, orNoneif the value isn’t a valid MAC.
With no arguments, getmac finds the default interface first (step 3 with default_iface methods, which read the routing table), then looks up its MAC. If that interface has no MAC (a VPN tunnel, for example), the first interface with a MAC that isn’t a loopback interface is used.
Three kinds of methods¶
The METHODS list in getmac/getmac.py is the registry, and its order is the order of preference within each type. Methods come in three kinds:
Files. On Linux and Android,
/sys/class/net/<iface>/addressgives an interface’s MAC,/proc/net/arpis the ARP table, and/proc/net/routeis the routing table. These are the fastest methods, so they’re preferred where they exist.System calls.
fcntlwithSIOCGIFHWADDRon Linux, andctypescalls intoiphlpapi.dllon Windows. No subprocess, no parsing of text.Commands.
ip,arp,ndp,ifconfig,netstat,route,arping,lanscan,networksetup,getmac.exe,ipconfig.exe,netsh.exe, andwmic.exe. getmac runs the command withsubprocess.check_output()(never through a shell), discards stderr, and searches stdout with a regular expression written for that command’s output format.arpingis tried when it’s installed (test()only checks that the command exists), so if it’s installed but getmac doesn’t have the privileges to use it (it usually needs root), the lookup returnsNoneinstead of falling back to the next method. See Do I need root or admin?.
Each method is small and independent: it declares what it supports, checks whether it can run, and parses one format. A new platform or command version gets a new method and a new fixture of real command output in tests/samples/, rather than a change to an existing method’s regular expression. That keeps a fix for one platform from breaking another.
Platform detection and PATH¶
Constants detects the platform once, at import time, from platform.uname(). PLATFORM is wsl on WSL1, linux on Linux, WSL2, and Android, and otherwise the lowercased system name (darwin, windows, freebsd, sunos, and so on). Methods are filtered on it. other isn’t something getmac detects, it’s the bucket of generic methods getmac falls back to (with a RuntimeWarning) when nothing matches. The names a method can list are in Method.VALID_PLATFORM_NAMES: android, darwin, linux, windows, wsl, openbsd, freebsd, netbsd, dragonfly, sunos, hp-ux, and other (see Platforms). OVERRIDE_PLATFORM replaces the detected value, which is how the test suite exercises every platform’s methods on one machine.
Commands are found on a search path (gvars.PATH) that’s copied from the process’s PATH at import time. On POSIX systems /sbin and /usr/sbin are added, since arp, ip, and ifconfig often live there and a normal user’s PATH may not include them. On Windows, Python Scripts directories are removed, so a pip-installed getmac.exe (this package’s own command) or ping.exe can’t be picked instead of the Windows command of the same name. That only changes getmac’s internal copy. Your shell’s PATH isn’t touched, which is why Troubleshooting still tells you to add Scripts to it. Commands run with a copy of the environment that sets LC_ALL=C, so their output isn’t translated. See Environment variables.
Singleton classes for global state¶
All of getmac’s global state lives in one module, getmac.variables, as attributes of three classes that are each instantiated exactly once:
settings(Settings): user-configurable options, such asDEBUGandPORT.consts(Constants): platform detection results that don’t change after import, such asWINDOWSandPLATFORM.gvars(Variables): everything else that changes at runtime, such asPATHandCHECK_COMMAND_CACHE.
getmac.py, utils.py and __main__.py all import the same three objects (from .variables import consts, gvars, settings), so every module reads and writes one shared copy of each value, instead of each module keeping its own.
Why objects, instead of the module-level variables that 0.9.x used?
No
globalkeyword. Code changes an attribute (gvars.DEFAULT_IFACE = ...) instead of rebinding a module-level name, so functions don’t needglobaldeclarations.Changes are seen everywhere. When you set
settings.DEBUG = 2(or the CLI does, from--debug), all of getmac sees it. A module-level variable copied withfrom ... import DEBUGwould keep its old value in every module that imported it.Tests are simpler. One patch, such as
mocker.patch.object(consts, "WINDOWS", True), changes the value everywhere it’s used, and is undone automatically when the test ends.Better docs. Each attribute has a type annotation and a docstring, and the classes are rendered in the API, which makes it clear which values are settings for users to change and which are internal.
Room to grow. An attribute can become a property that validates or lazily computes its value, and the singletons could later be replaced by instances passed to functions (as planned for
OVERRIDE_PLATFORM), without changing thesettings.NAMEstyle of access in the code that uses them.