40
\$\begingroup\$

randip.py

I know there are probably better, easier ways to do this.
It was just a bit of a learning exercise for the sake of familiarising myself with Python.

It takes a single argument (positional parameter):
Either a 4 (IPv4) or a 6 (IPv6).


Usage:

./randip.py 4
61.104.170.242

./randip.py 6
4bfc:391d:3ec8:68ef:0ec8:529b:166d:2ece

Code:

#!/usr/bin/env python3

from sys import argv

from random import randint, choice
from string import hexdigits

def random_ip(v):

    if v == 4:
        octets = []
        for x in range(4):
            octets.append(str(randint(0,255)))
        return '.'.join(octets)

    elif v == 6:
        octets = []
        for x in range(8):
            octet = []
            for x in range(4):
                octet.append(str(choice(hexdigits.lower())))
            octets.append(''.join(octet))
        return ':'.join(octets)

    else:
       return 

def main(): 
    print(random_ip(int(argv[1])))

if __name__ == '__main__':
    main()
\$\endgroup\$
0

6 Answers 6

54
\$\begingroup\$

Python is often described as a "batteries included" kind of language, and this is no exception.

There's a module just for IP address manipulation and another module to generate random numbers. Put together, they do exactly what you want, in a way that's slightly more readable (IMO).

For this example, I'll assume that the variable v contains either 4 or 6.

from random import getrandbits
from ipaddress import IPv4Address, IPv6Address

if v == 4:
    bits = getrandbits(32) # generates an integer with 32 random bits
    addr = IPv4Address(bits) # instances an IPv4Address object from those bits
    addr_str = str(addr) # get the IPv4Address object's string representation
elif v == 6:
    bits = getrandbits(128) # generates an integer with 128 random bits
    addr = IPv6Address(bits) # instances an IPv6Address object from those bits
    # .compressed contains the short version of the IPv6 address
    # str(addr) always returns the short address
    # .exploded is the opposite of this, always returning the full address with all-zero groups and so on
    addr_str = addr.compressed 

print(addr_str)

Here, addr_str will hold a fully random IPv4 or IPv6 address.

You can even generate random addresses from a subnet like this:

from random import getrandbits
from ipaddress import IPv4Network, IPv4Address

# network containing all addresses from 10.0.0.0 to 10.0.0.255
subnet = IPv4Network("10.0.0.0/24") 

# subnet.max_prefixlen contains 32 for IPv4 subnets and 128 for IPv6 subnets
# subnet.prefixlen is 24 in this case, so we'll generate only 8 random bits
bits = getrandbits(subnet.max_prefixlen - subnet.prefixlen)

# here, we combine the subnet and the random bits
# to get an IP address from the previously specified subnet
addr = IPv4Address(subnet.network_address + bits)
addr_str = str(addr)

print(addr_str)

Here, addr_str will always contain IP addresses like 10.0.0.184, 10.0.0.42 and so on. It works the same way with IPv6 addresses, except in that case you'd have to import IPv6Network and IPv6Address instead.

\$\endgroup\$
10
  • 7
    \$\begingroup\$ Nice solution - did you consider pushing addr_str = addr.compressed to after the conditional? \$\endgroup\$
    – cmh
    Jul 26, 2018 at 15:07
  • 4
    \$\begingroup\$ @tjt263 Yeah, I'd say this might have less educational value than your approach, even though it is a much better way to just generate a random IP address if that's what you really want. \$\endgroup\$
    – David Z
    Jul 26, 2018 at 17:46
  • 6
    \$\begingroup\$ In CodeReview users aren't trying to do something as efficiently as possible with builtins, but rather get the form of their approach right. Telling someone who's writing an IP generator from scratch to just import it doesn't really serve the community. \$\endgroup\$ Jul 26, 2018 at 20:42
  • 7
    \$\begingroup\$ Before posting it, I considered prefacing my answer with something like "This isn't a proper answer for Code Review, but...", but I didn't think it'd blow up like this. I intended it to be a helpful resource for future people finding this via Google, because just a few months ago, I was in the exact same situation as OP: wanting to write an IP randomizer, not knowing Python could do it for me. I've added a few comments to my examples, hopefully they'll make it more obvious how the code works. \$\endgroup\$
    – emmalyx
    Jul 27, 2018 at 6:58
  • 2
    \$\begingroup\$ @user1717828 That's not true. If you are open to feedback on CRSE, you are inherently open to alternative solutions to the problem. If you purposefully choose to reinvent the wheel (as a personal challenge or otherwise), there's the reinventing-the-wheel tag. \$\endgroup\$
    – Daniel
    Jul 27, 2018 at 23:12
26
\$\begingroup\$

Here are a few ideas about your code.

Check for command line arguments

The code fails with an exception if it's invoked with no command line arguments because it attempts to use argv[1] and there isn't any. I'd suggest that it would be nice to print a "usage" message if the user enters either an invalid or no argument.

Use a list comprehension

List comprehensions are extremely useful and very Pythonic. It's really good to become proficient with them. Here's how to use one to generate a random IPv4 address:

'.'.join([str(randint(0,255)) for x in range(4)])

And an IPv6 address is a little trickier because we need hex digits.

':'.join([hex(randint(2**16,2**17))[-4:] for x in range(8)])

That works because randint generates a number in the range 0x10000 to 0x20000 and we then pick off the last four hex digits.

\$\endgroup\$
5
  • \$\begingroup\$ Thanks. That last part's a bit clever. I had a similar idea, but wasn't able to pull it off. The list comprehensions are a nice touch too, just a bit trickier to articulate. \$\endgroup\$
    – voices
    Jul 26, 2018 at 13:37
  • 4
    \$\begingroup\$ you can use 0x10000 and 0x20000 instead of 2**16 and 2**17 for better readability. \$\endgroup\$ Jul 26, 2018 at 13:40
  • 2
    \$\begingroup\$ It might be worth mentioning that you don't need the list comprehension to really be a list. It could be a generator instead. \$\endgroup\$
    – David Z
    Jul 26, 2018 at 17:48
  • \$\begingroup\$ For IPv6, leading zeroes in a group can be omitted, so you don't need that trick, and just do ':'.join(hex(randrange(0x10000))[2:] for x in range(8)) \$\endgroup\$
    – Jasmijn
    Jul 27, 2018 at 12:38
  • \$\begingroup\$ @Robin: it's true, but I like to make it appear consistent. Personally, I prefer PeterW. 's answer. Very elegant! \$\endgroup\$
    – Edward
    Jul 27, 2018 at 12:40
19
\$\begingroup\$

For someone not familiar with Python, you have picked pretty good habits. Not everyone uses functions or the if __name__ == '__main__' guard first try.

That being said, I think it would make more sense to provide 2 functions instead of a single one: random_ipv4 and random_ipv6.

You could also feed generator expressions to join. They are both faster to process than [] + for + append and easier to read:

def random_ipv4():
    return '.'.join(str(randint(0,255)) for _ in range(4))


def random_ipv6():
    return ':'.join(
            ''.join(choice(hexdigits).lower() for _ in range(4))
            for _ in range(8)
    )

The only thing left being to properly validate that the input is either 4 or 6:

if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('Usage: python random_ip.py VERSION')

    version = sys.argv[1]
    if version == '4':
        print(random_ipv4())
    elif version == '6':
        print(random_ipv6())
    else:
        sys.exit('VERSION should be 4 or 6, not {}'.format(version))
\$\endgroup\$
4
  • \$\begingroup\$ CMIIW, but aren't list comprehension a bit faster than generator comprehension when memory is not an issue? \$\endgroup\$
    – Ludisposed
    Jul 26, 2018 at 14:01
  • \$\begingroup\$ @Ludisposed Found old timings at bytes.com/topic/python/answers/… But for join it shouldn't matter much as the current implementation convert the input to a list right away. \$\endgroup\$ Jul 26, 2018 at 14:49
  • \$\begingroup\$ @Ludisposed My own testing indicates 5.792979179001122 seconds for 1 million calls to random_ipv4 using a generator expression and 5.526552320003248 using a list-comprehension. Nothing really conclusive to prefer one over the other. And since we aren't to re-use the result, a generator expression is a good indication of that. \$\endgroup\$ Jul 26, 2018 at 15:01
  • 1
    \$\begingroup\$ Yes it shouldn't matter much and is dependent on the data. I agree with your reasoning for choosing a generator. \$\endgroup\$
    – Ludisposed
    Jul 26, 2018 at 15:11
14
\$\begingroup\$

Check for invalid outputs, and regenerate them.

It's possible that you'll produce an output that's reserved for a particular purpose, such as loopback (127.0.0.1 or ::1) or broadcast (255.255.255.255, ff02::1, ff02::2). Build in knowledge of such addresses, and if you find you've produced one, then replace it. You can do that recursively:

def make_address(v):
    address = random_ip(v)
    if is_reserved(address):
        return make_address(v)
    return address
   
\$\endgroup\$
4
  • 2
    \$\begingroup\$ There is no need for this to be recursive. It might as well be a while is_reserved(address): loop. But then again, if the chance to generate a valid IP is only a per mille (so you are in danger of regularly hitting the stack size limit), you are probably doing something wrong... \$\endgroup\$
    – Graipher
    Jul 26, 2018 at 15:20
  • 1
    \$\begingroup\$ Yes, I was just showing one way. Python's only an occasional language for me, and I couldn't remember whether it guarantees tail-call elimination... \$\endgroup\$ Jul 26, 2018 at 16:13
  • \$\begingroup\$ Maybe you want extra options to create IPv6 addresses formed from mapping IPv4 addresses and from hardware MAC addresses, then. \$\endgroup\$ Jul 26, 2018 at 16:59
  • 1
    \$\begingroup\$ @TobySpeight It doesn't, but there is a way to guarantee it manually, by using a decorator: stackoverflow.com/q/27417874/2415524 \$\endgroup\$
    – mbomb007
    Jul 27, 2018 at 21:52
12
\$\begingroup\$

Validate the input: At present, your program

  • aborts with IndexError if called without arguments,
  • aborts with ValueError if called with a non-integer argument,
  • prints None if called with an integer argument that is not 4 or 6.

Missing or invalid arguments should print a helpful error message. Most Unix command-line tools print the message to the standard error and terminate with a non-zero exit status in the case of a failure.

It would be easier to compare the given argument against the strings "4" and "6" instead of converting it to an integer (which can fail).


Use list comprehension instead of appending to an array in a loop. Use _ as iterator variable if the concrete value is not needed.

As an example, the "IPv4" case can be implemented as

if v == 4:
    return '.'.join(str(randint(0,255)) for _ in range(4))
\$\endgroup\$
2
  • \$\begingroup\$ Why _? Is that a thing people do? I've never seen it before. \$\endgroup\$
    – voices
    Jul 26, 2018 at 17:45
  • 2
    \$\begingroup\$ @tjt263: It is just by convention used as a “throw-away variable,” see for example stackoverflow.com/q/5893163/1187415 \$\endgroup\$
    – Martin R
    Jul 26, 2018 at 18:27
8
\$\begingroup\$

Since you already got a couple of answers telling you to validate your inputs, here is a way how to do it using the argparse module:

import argparse

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-v',
                        type=int,
                        choices=(4, 6),
                        default=4,
                        help="Whether to generate a random IPv4 or IPv6 address. Default: IPv4.")
    args = parser.parse_args()
    print(random_ip(args.v))

You can then use it on the command line like this:

./randip
./randip -v 4
./randip -v4
./randip -v 6

If you do anything else, a helpful usage message is printed:

usage: [-h] [-v {4,6}]

optional arguments:
  -h, --help  show this help message and exit
  -v {4,6}    Whether to generate a random IPv4 or IPv6 address. Default:
              IPv4.
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.