Generating a random MAC address I need the ability to generate random MAC addresses, so I wrote up a little function that does that:
&gt;&gt;&gt; random_mac()
'7C:93:B7:AF:BA:AE'
&gt;&gt;&gt; random_mac()
'D8:D8:A0:D4:A5:3F'
&gt;&gt;&gt; random_mac(unicast=False, universal=True)
'55:47:C6:EE:C6:2B'
&gt;&gt;&gt; random_mac(unicast=True, universal=False)
'FE:A1:4B:98:76:B6'

I decided to allow the user to pick if it's unicast/multicast and universally/locally administered; even though I'll only need unicast/universal. This caused me headaches though because I'm still not great at dealing with bits. The LSB of the first octet indicates uni/multicast, and the second LSB of the octet indicates universal/local, so these bits can't be random.
After playing around with a few failed ideas (generating all random bits, then &quot;fixing&quot; the two bits later), I finally decided to generate a random number between 0 and 63 inclusive, left shift it twice, than add the two bits on after. It works, but it's ugly and looks suboptimal.
It's not a lot of code, but I'd like a few things reviewed:

Is there a better approach? It feels hacky generating it as two pieces then adding them together. I tried explicitly setting the bits, but the code to decide between |, and &amp; and ~ got messier than what I have now, so I went with this way.
The number constants are bugging me too. The numbers kind of sit on a border of self-explanatory and magic, so I decided to name them to be safe. LAST_SIX_BITS_VALUE feels off though.
Is treating a boolean value as a number during bitwise operation idiomatic? Is it clear as I have it now?
Attaching the first octet to the rest is suboptimal as well. Speed isn't a huge concern, but I'm curious if there's a cleaner way that I'm missing.


from random import randint, randrange

N_MAC_OCTETS = 6
OCTET_VALUE = 256
LAST_SIX_BITS_VALUE = 63

def random_mac(unicast: bool = True, universal: bool = True) -&gt; str:
    least_two_bits = (not unicast) + ((not universal) &lt;&lt; 1)
    first_octet = least_two_bits + (randint(0, LAST_SIX_BITS_VALUE) &lt;&lt; 2)
    octets = [first_octet] + [randrange(OCTET_VALUE) for _ in range(N_MAC_OCTETS - 1)]
    return &quot;:&quot;.join(f&quot;{octet:02X}&quot; for octet in octets)


Examples of the bits for the first octet for different inputs:
def display(mac):
    print(mac, f&quot;{int(mac.split(':')[0], 16):08b}&quot;)

# Unicast, Universal
&gt;&gt;&gt; display(random_mac(True, True))
04:27:DE:9A:1B:D7 00000100  # Ends with 0,0

# Unicast, Local
&gt;&gt;&gt; display(random_mac(True, False))
72:FB:49:43:D5:F2 01110010  # 1,0

# Multicast, Universal
&gt;&gt;&gt; display(random_mac(False, True))
7D:BF:03:4E:E5:2A 01111101  # 0,1

# Multicast, Local
&gt;&gt;&gt; display(random_mac(False, False))
2F:73:52:12:8C:50 00101111  # 1,1
Negating an argument is somewhat counterintuitive. Consider passing them as multicast and local instead.

I would seriously consider defining
  UNIVERSAL = 0x01
  MULTICAST = 0x02

and pass them as a single argument, is in
      random_mac(UNIVERSAL | MULTICAST)


Using both randint and randrange feels odd. I would stick with randrange.

First octet needs a special treatment anyway. That said, consider
  def random_mac(special_bits = 0):
      octets = [randrange(OCTET_VALUE) for _ in range(N_MAC_OCTETS)]
      octets[0] = fix_octet(octet[0], special_bits)
      return &quot;:&quot;.join(f&quot;{octet:02X}&quot; for octet in octets)

with
  def fix_octet(octet, special_bits):
      return (octet &amp; ~0x03) | special_bits