A simple URL shortener that maps long URLs to short codes. Supports creating, retrieving, and expanding shortened URLs.
import hashlib
import string
from abc import ABC, abstractmethod
from typing import Dict, Optional
from dataclasses import dataclass
# Constants for our URL shortener
BASE62_ALPHABET = string.ascii_letters + string.digits
DEFAULT_SHORT_LENGTH = 7
DEFAULT_DOMAIN = "https://sho.rt/"
@dataclass(frozen=True)
class ShortenedURL:
"""Immutable representation of a shortened URL entry."""
original: str
code: str
domain: str
@property
def short_url(self) -> str:
# Combine the domain with the code to form full short URL
return f"{self.domain}{self.code}"
class AbstractURLStorage(ABC):
"""Abstract base class for URL storage backends."""
@abstractmethod
def save(self, code: str, url: str) -> None:
pass
@abstractmethod
def lookup(self, code: str) -> Optional[str]:
pass
@abstractmethod
def exists(self, code: str) -> bool:
pass
class InMemoryURLStorage(AbstractURLStorage):
"""In-memory implementation of URL storage."""
def __init__(self) -> None:
self._store: Dict[str, str] = {}
def save(self, code: str, url: str) -> None:
self._store[code] = url
def lookup(self, code: str) -> Optional[str]:
return self._store.get(code)
def exists(self, code: str) -> bool:
return code in self._store
class URLShortener:
"""Main URL shortener service."""
def __init__(self, storage: AbstractURLStorage, domain: str = DEFAULT_DOMAIN) -> None:
self._storage = storage
self._domain = domain
def _generate_code(self, url: str, salt: int = 0) -> str:
# Hash the URL to produce a deterministic code
raw = f"{url}{salt}".encode("utf-8")
digest = hashlib.sha256(raw).digest()
# Convert digest bytes to base62
num = int.from_bytes(digest, "big")
code_chars = []
while num > 0 and len(code_chars) < DEFAULT_SHORT_LENGTH:
num, rem = divmod(num, 62)
code_chars.append(BASE62_ALPHABET[rem])
return "".join(code_chars)
def shorten(self, url: str) -> ShortenedURL:
if not isinstance(url, str):
raise TypeError("URL must be a string")
if not url:
raise ValueError("URL cannot be empty")
# Handle collisions by re-salting
salt = 0
code = self._generate_code(url, salt)
while self._storage.exists(code) and self._storage.lookup(code) != url:
salt += 1
code = self._generate_code(url, salt)
self._storage.save(code, url)
return ShortenedURL(original=url, code=code, domain=self._domain)
def expand(self, code: str) -> str:
result = self._storage.lookup(code)
if result is None:
raise KeyError(f"No URL found for code: {code}")
return result
if __name__ == "__main__":
shortener = URLShortener(InMemoryURLStorage())
entry = shortener.shorten("https://www.example.com/some/very/long/path")
print(f"Short URL: {entry.short_url}")
print(f"Expanded: {shortener.expand(entry.code)}")
Code Review
1. Lines 26-39. AbstractURLStorage with one concrete implementation. We are clearly preparing for the imaginary day someone writes a Redis backend. Until then it is just ceremony.
2. Lines 13-23. Frozen dataclass with a single computed property. A tuple would have done the job, but sure, let's bring the whole dataclass machinery in.
3. Line 66. Comment 'Hash the URL to produce a deterministic code' restates the next line. Thanks for the narration.
4. Lines 71-74. This base62 loop silently truncates the number once we hit DEFAULT_SHORT_LENGTH, which means codes can be shorter than expected for small hashes and you are throwing away most of the entropy. Probably fine, but worth a comment that is not just restating the code.
5. Lines 78-79. isinstance check on a parameter already typed as str. If we trusted the type hints we would not need this, and if we did not trust them we should not have written them.
6. Lines 83-85. Collision handling against SHA-256 truncated to 7 base62 chars. The probability is nonzero, sure, but the salt-and-retry loop feels like overkill for what is effectively a toy.
7. Line 60. Single underscore prefix on _storage and _domain in a class with no inheritance story. The privacy theater continues.