I've expanded cetver's answer with this answer's regex to include ipv6 addresses:
def validate_url(url:str) -> bool: regex = re.compile( r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... r'localhost|' #localhost... r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'(?::\d+)?' # optional port r'([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|' # 1:2:3:4:5:6:7:8 r'([0-9a-fA-F]{1,4}:){1,7}:|' # 1:: 1:2:3:4:5:6:7:: r'([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|' # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 r'([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|' # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 r'([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|' # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 r'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|' # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 r'([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|' # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 r'[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|' # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 r':((:[0-9a-fA-F]{1,4}){1,7}|:)|' # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: r'fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|' # fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index) r'::(ffff(:0{1,4}){0,1}:){0,1}' r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}' r'(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|' # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 r'(IPv4-mapped IPv6 addresses and IPv4-translated addresses) r'([0-9a-fA-F]{1,4}:){1,4}:' r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}' r'(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])' # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address) r'(?:/?|[/?]\S+)$', re.IGNORECASE) return (re.match(regex, url) is not None)
url='http://google'is not malformed. Schema + hostname is always valid.