I usually notice people reach for a logarithm in Python right after something breaks: a probability underflows to 0.0, a metric with a huge range becomes unreadable, or a formula that looked fine on paper turns into a swamp of floating-point edge cases. The good news is that Python’s standard library gives you solid, fast log functions that cover most day-to-day needs. The less good news is that it’s easy to pick the right function but still get the wrong result if you ignore domains, precision near zero, or the difference between scalar math and array math.\n\nWhen I’m writing production Python in 2026—data pipelines, model training code, finance scripts, scientific tools—I treat logs as a practical engineering tool, not just math trivia. If you stick with a few reliable patterns, you’ll get correctness and stability without adding complexity.\n\nBy the end of this post, you’ll be able to choose between math.log, math.log2, math.log10, and math.log1p, handle ugly inputs safely, and apply logs in real workloads (NumPy/Pandas/Polars and ML stacks) while keeping numeric behavior predictable.\n\n## A working mental model (why logs matter in code)\nA logarithm answers a simple question: what exponent do I raise a base to in order to get a number?\n\n- logb(a) = x means bx == a\n- The natural log is base e (Euler’s number). In Python, that’s the default for math.log.\n\nIn code, the two reasons I reach for logs are:\n\n1) Turning multiplication into addition\nIf you’re multiplying many small numbers (common in likelihoods or chained probabilities), floating-point numbers can underflow to 0.0. Taking logs converts:\n\n- p = p1 p2 p3 ... into log(p) = log(p1) + log(p2) + log(p3) + ...\n\n2) Compressing large numeric ranges for humans and models\nIf values span orders of magnitude—say, 1 to 10^12—log scaling makes patterns visible and reduces the dominance of extreme values.\n\nA quick practical note: the moment you apply a log, you’ve changed the meaning of the value. That’s fine, but you should make that change explicit in variable names (logprice, loglikelihood) and in any exported schema.\n\n## math.log: natural log, plus custom bases when you truly need them\nPython’s math module is the right starting point for scalar logs. The flagship is:\n\n- math.log(x) → natural log (base e)\n- math.log(x, base) → log of x in the given base\n\nUnder the hood, the two-argument form is computed in terms of the natural log (conceptually log(x) / log(base)). That’s convenient, but I don’t treat it as my default.\n\n### Natural log in real code\nNatural log shows up everywhere: continuous growth/decay, entropy-like formulas, ML loss functions, and many probability models.\n\npython\nimport math\n\nvalue = 14\nprint(‘ln(14) =‘, math.log(value))\n\n\n### Custom base: readability vs. consistency\nCustom bases are helpful when the base is meaningful to humans:\n\n- base 10 for decimal digits and scientific notation\n- base 2 for bits, binary trees, and information measures\n\nFor a one-off calculation, math.log(x, base) is fine:\n\npython\nimport math\n\nx = 14\nprint(‘log base 5 of 14 =‘, math.log(x, 5))\n\n\nBut in production code, if you care about base 2 or base 10, I recommend the dedicated functions (math.log2, math.log10) for clarity and numeric behavior.\n\n### Change-of-base without surprises\nIf you want to be explicit (and avoid the two-argument math.log), write the change-of-base yourself:\n\npython\nimport math\n\ndef logbase(x: float, base: float) -> float:\n # Explicit change of base: logb(x) = ln(x) / ln(b)\n return math.log(x) / math.log(base)\n\nprint(logbase(14.0, 5.0))\n\n\nI like this style when I want a single place to add validation (like rejecting base <= 0 or base == 1).\n\n### Domain rules you should internalize\nFor math.log (and friends), the domain is:\n\n- x > 0 is valid\n- x == 0 raises ValueError (domain error)\n- x < 0 raises ValueError (domain error)\n\nThat’s not Python being picky; that’s the real-valued logarithm. If you want complex logs (where negatives are allowed), you should switch to cmath (I’ll show this later).\n\n## math.log2 and math.log10: pick them on purpose\nWhen you specifically want base 2 or base 10, I recommend:\n\n- math.log2(x)\n- math.log10(x)\n\nThese functions communicate intent immediately. When I’m reading code in a hurry, that matters.\n\n### log2: bits, powers of two, and ML-ish work\npython\nimport math\n\nx = 14\nprint(‘log2(14) =‘, math.log2(x))\n\n\nPlaces I commonly see log2:\n\n- Estimating complexity in algorithms (log2(n) levels)\n- Reasoning about storage (log2(states))\n- Interpreting binary scaling (doublings)\n\n### log10: digits, orders of magnitude, and reporting\npython\nimport math\n\nx = 14\nprint(‘log10(14) =‘, math.log10(x))\n\n\n#### Practical pattern: number of digits in an integer\nThis is a classic, and it’s still handy when you’re formatting output or building fixed-width encodings.\n\npython\nimport math\n\ndef decimaldigits(n: int) -> int:\n if n == 0:\n return 1\n if n < 0:\n n = -n\n return int(math.log10(n)) + 1\n\nprint(decimaldigits(73293))\n\n\nI also keep a safer alternative in mind: len(str(abs(n))) is slower but avoids floating-point corner cases for extremely large integers. If your integers can be huge (think cryptography or arbitrary-precision identifiers), string length is often the simplest correct tool.\n\n### When I avoid logs for digit counting\nIf I’m already holding a Python int, computing digits through logs can go wrong when the integer is so large that the float conversion loses detail. For everyday sizes, it’s fine. For very large ints, I do:\n\n- digits = len(str(abs(n))) (simple and correct)\n\n## math.log1p: the function that quietly saves your numeric stability\nIf I had to pick one log function people underuse, it’s math.log1p(x), which computes:\n\n- log(1 + x) (natural log)\n\nThat sounds boring until you hit the classic floating-point issue: when x is very small, 1 + x rounds to 1.0, and log(1.0) becomes 0.0. That’s not what you want.\n\n### Why log1p exists (a tiny example with a big consequence)\npython\nimport math\n\nx = 1e-16\nnaive = math.log(1.0 + x)\nstable = math.log1p(x)\n\nprint(‘naive =‘, naive)\nprint(‘log1p =‘, stable)\n\n\nOn many systems, the naive result prints 0.0 because 1.0 + 1e-16 can round back to 1.0 in binary floating-point. log1p uses a more stable algorithm that preserves information in that range.\n\n### Where I reach for log1p in production\n- Log returns: log(1 + r) where r is a small daily return\n- Loss functions that contain log(1 + exp(x))-style shapes\n- Any time I see log(1 + somethingsmall)\n\n### Companion function worth knowing: expm1\nNot a log, but it pairs with log1p:\n\n- math.expm1(x) computes exp(x) - 1 accurately for small x\n\nWhen I’m moving between log-space and normal space around zero, log1p and expm1 prevent a lot of subtle drift.\n\n## Exceptions, edge cases, and input validation I actually ship\nLogs are unforgiving about domain. Real data is unforgiving about being clean. The trick is to make your policy explicit.\n\n### What happens with invalid inputs\nFor real-valued logs in math:\n\n- math.log(-14) → ValueError: math domain error\n- math.log(0) → ValueError: math domain error\n\nThis is a good default because it fails loudly.\n\n### My go-to pattern: a strict function plus a safe wrapper\nI like to keep one strict function that raises, and a wrapper that applies a clear policy.\n\npython\nimport math\nfrom typing import Optional\n\ndef strictln(x: float) -> float:\n # Raises ValueError if x <= 0\n return math.log(x)\n\ndef safeln(x: float, , oninvalid: Optional[float] = None) -> Optional[float]:\n # Policy: return oninvalid if x <= 0 or x is not finite.\n # If oninvalid is None, the caller can decide how to handle missing values.\n if not math.isfinite(x) or x <= 0.0:\n return oninvalid\n return math.log(x)\n\nprint(safeln(10.0))\nprint(safeln(0.0, oninvalid=float(‘-inf‘)))\nprint(safeln(-3.0, oninvalid=None))\n\n\nYou might choose different policies depending on the context:\n\n- Data cleaning / feature engineering: return None or nan and filter later\n- Log-likelihood math: treat log(0) as -inf if that matches your model\n- Finance reporting: reject invalid values early and raise\n\nThe important part is that you decide, document it, and test it.\n\n### Handling zeros: -inf is often the right answer\nIf a value is exactly 0 in a probability context, log(0) is -inf. In floating point:\n\n- You can represent -inf as float(‘-inf‘)\n- You can propagate it through sums (a lot of log-space code is written with this in mind)\n\n### Negative values: do you want complex logs?\nIf you need logs of negatives in a mathematically meaningful way, you probably want complex arithmetic.\n\npython\nimport cmath\n\nx = -14\nprint(‘complex log(-14) =‘, cmath.log(x))\nprint(‘complex log10(-14) =‘, cmath.log10(x))\n\n\nThis returns a complex number because the log of a negative real has an imaginary component (related to angles on the complex plane). I only do this when the domain truly demands it (signal processing, control theory, some physics/math workloads). For business analytics, complex results are usually a data quality problem.\n\n### NaN and infinity: don’t guess\nmath.isfinite(x) is your friend. When I see nan or inf in inputs, I treat it as an upstream bug unless I have a strong reason not to.\n\n## Scalar vs array math: math vs NumPy/Pandas/Polars (and friends)\nmath is for scalars. The moment you have arrays, you should switch to array-aware functions.\n\n### A quick comparison table\n
Best default
\n
—
\n
math.log, math.log2, math.log10, math.log1p
\n
numpy.log, numpy.log2, numpy.log10, numpy.log1p
where= masking \n
numpy.log(...) or Series.map/Series.apply carefully
apply for speed \n
pl.col(‘x‘).log() / expressions
\n
torch.log, jax.numpy.log
\n\n### NumPy: vectorized logs (with a sane invalid policy)\npython\nimport numpy as np\n\nvalues = np.array([10.0, 1.0, 0.0, -5.0, np.nan])\n\n# Policy: compute ln(x) only where x > 0 and finite, else set NaN\nmask = np.isfinite(values) & (values > 0.0)\nout = np.full(values.shape, np.nan, dtype=float)\nout[mask] = np.log(values[mask])\n\nprint(out)\n\n\nIf you want a concise approach, NumPy’s where parameter is useful:\n\npython\nimport numpy as np\n\nvalues = np.array([10.0, 1.0, 0.0, -5.0])\n\nmask = values > 0.0\nout = np.log(values, where=mask, out=np.fulllike(values, np.nan))\nprint(out)\n\n\n### Pandas: avoid .apply for big columns\nFor large datasets, .apply calls Python per row and can be slow. I typically do:\n\npython\nimport numpy as np\nimport pandas as pd\n\ndf = pd.DataFrame({‘revenue‘: [100.0, 250.0, 0.0, 50.0]})\n\n# Example policy: ln(revenue) for revenue > 0, else NaN\nrevenue = df[‘revenue‘].tonumpy(dtype=float)\nmask = revenue > 0.0\nlogrevenue = np.full(revenue.shape, np.nan)\nlogrevenue[mask] = np.log(revenue[mask])\n\ndf[‘logrevenue‘] = logrevenue\nprint(df)\n\n\n### Polars: keep it in expressions\nIf you’re using Polars (common in 2026 data stacks), I recommend sticking to expression syntax so you keep lazy execution and query planning.\n\nConceptually:\n\n- compute log only when x > 0\n- otherwise return null\n\n(Exact expression names can vary slightly by version, but the pattern is stable: conditional expression + log expression.)\n\n## Real-world patterns I recommend: log-space math and stable probability code\nThis is where logs stop being a math function and become an engineering pattern.\n\n### Pattern 1: multiplying many probabilities\nInstead of multiplying probabilities directly:\n\npython\np = 0.1 0.2 0.05 0.01\n\n\nI do:\n\npython\nimport math\n\nprobs = [0.1, 0.2, 0.05, 0.01]\nlogp = sum(math.log(p) for p in probs)\n\nprecovered = math.exp(logp)\nprint(‘logp =‘, logp)\nprint(‘p =‘, precovered)\n\n\nThis buys you range and stability. You can sum hundreds or thousands of log-probabilities without underflowing to zero.\n\n### Pattern 2: log-likelihoods and clear naming\nIf you’re working with models (even simple ones), keep things in log-space and name them that way:\n\n- loglikelihood\n- logprior\n- logposterior\n\nWhen I review code, that naming prevents entire classes of bugs.\n\n### Pattern 3: stable log-sum-exp (the quiet MVP)\nA classic numeric problem: you have log-values and want the log of the sum of exponentials.\n\nNaive:\n- log(sum(exp(li))) can overflow if any li is large\n\nStable trick:\n- subtract the max first\n\npython\nimport math\nfrom typing import Iterable\n\ndef logsumexp(logvalues: Iterable[float]) -> float:\n logvalues = list(logvalues)\n if not logvalues:\n return float(‘-inf‘)\n\n m = max(logvalues)\n if m == float(‘-inf‘):\n return float(‘-inf‘)\n\n total = 0.0\n for v in logvalues:\n total += math.exp(v - m)\n return m + math.log(total)\n\nprint(logsumexp([math.log(0.1), math.log(0.2), math.log(0.3)]))\n\n\nIf you’re already using SciPy, scipy.special.logsumexp is well-tested and fast. I still like keeping a small local implementation for minimal-dependency services.\n\n### Pattern 4: softmax without overflow\nSoftmax is another place where people accidentally create inf.\n\npython\nimport math\nfrom typing import List\n\ndef softmax(scores: List[float]) -> List[float]:\n m = max(scores)\n exps = [math.exp(s - m) for s in scores]\n denom = sum(exps)\n return [e / denom for e in exps]\n\nprint(softmax([1000.0, 1001.0, 1002.0]))\n\n\nNotice the same idea: shift by the max before exponentiating.\n\n## Common mistakes (and what I do instead)\n### Mistake 1: calling math.log on data that includes zeros\nIf zeros are possible, you need a policy:\n\n- If zeros mean missing/invalid, map to nan and handle later\n- If zeros are valid but represent impossibility (probability 0), map to -inf\n\nI write that policy once (a helper function) and reuse it.\n\n### Mistake 2: log(1 + x) instead of log1p(x) for small x\nIf x can be near 0, log1p is the safer choice.\n\n### Mistake 3: mixing bases casually\nIf you compute something in natural log and later compare it to a base-10 log without converting, you’ll get nonsense. I recommend choosing one base for internal math (usually natural log) and converting only at boundaries (display, reports, APIs).\n\nConversion reminders:\n\n- log10(x) = ln(x) / ln(10)\n- log2(x) = ln(x) / ln(2)\n\n### Mistake 4: expecting logs to fix bad distributions by magic\nApplying a log changes scale and can make patterns clearer, but it can also hide important differences. If you’re feeding logs into a model, keep three things in mind:\n\n1) A log transform is not a substitute for data quality. If your metric has zeros because of missing data, or negatives because of refunds/chargebacks/adjustments, log won’t “solve” that—your policy will.\n\n2) You can introduce bias with the wrong offset. If you do log(x + 1) without thinking, you’ve decided that adding 1 is the right way to handle zeros. That might be reasonable for counts; it’s often nonsense for dollars.\n\n3) Interpretability changes. Linear differences in log-space correspond to multiplicative differences in the original space. That can be exactly what you want (e.g., “10% growth”), but you need to explain it.\n\n### Mistake 5: forgetting the inverse transform (and shipping wrong outputs)\nThis one shows up constantly in forecasting and ML systems: you train on log(y) and then forget to exponentiate the prediction back to y.\n\nEven worse: you do exponentiate, but you forget that E[exp(Z)] is not exp(E[Z]) when Z has variance (Jensen’s inequality). In practice, this can cause systematic underprediction after you invert back to the original scale.\n\nI handle this by:\n\n- Naming targets clearly (logtarget)\n- Centralizing transforms/inverse-transforms\n- Writing a small unit test that checks “round-trip” behavior: inverse(transform(x)) ≈ x for typical and extreme values\n\n## Deeper domain rules (bases, signs, and what’s actually allowed)\nIf you’re only ever logging positive floats, life is easy. Real production inputs are rarely that polite. Here’s the mental checklist I run through.\n\n### The base must be valid too\nIf you use math.log(x, base) (or your own change-of-base helper), you need to validate the base:\n\n- base > 0\n- base != 1\n\nIf base is 1, the “what exponent produces x?” question is undefined, because 1anything == 1.\n\n### Logging integers is fine—until floats get involved\nPython’s math.log accepts int inputs and does the right thing mathematically, but the result is a float, and extremely large integers will eventually run into floating-point limitations. Two practical rules I use:\n\n- For everyday ints (IDs, counts, money in cents within reasonable bounds), math.log is fine.\n- For truly huge ints (cryptographic sizes, huge combinatorics), I avoid float-based log logic unless I’ve explicitly tested the error tolerance.\n\n### “Signed log” transforms for mixed-sign data\nSometimes you have a feature that legitimately goes negative (profit/loss, net cash flow, deltas). Real-valued log(x) can’t handle negatives, but you can still compress magnitude while keeping sign by using a signed transform. I’ve used this pattern for feature engineering and visualization:\n\n- signedlog1p(x) = sign(x) log1p(abs(x))\n\npython\nimport math\n\ndef signedlog1p(x: float) -> float:\n if x == 0.0:\n return 0.0\n return math.copysign(math.log1p(abs(x)), x)\n\nfor v in [-1000.0, -10.0, -0.1, 0.0, 0.1, 10.0, 1000.0]:\n print(v, ‘->‘, signedlog1p(v))\n\n\nThis is not “the logarithm” in the strict math sense, but it’s a very practical scaling function when your goal is to tame dynamic range without discarding sign.\n\n## Precision: what changes near zero, near one, and at extreme magnitudes\nFloating-point is usually good enough—until it isn’t. Logs are one of the first places precision problems become visible.\n\n### Near 1.0: use log1p when the input is “1 + tiny”\nIf you need log(1 + x) and x can be tiny, log1p(x) is the right call. I treat it like decimal rounding rules: if I don’t explicitly choose the stable function, I’m accepting avoidable error.\n\n### Near 0.0: decide what 0 means\nIn probability and information theory code, 0 frequently means “impossible.” In that world, mapping log(0) to -inf is often correct and expected. In business metrics, 0 might mean “no events,” “no sales,” or “missing.” Those are not the same thing, and logs force you to choose.\n\n### Extremely large values: log may be safer than exp\nA subtle but useful point: exp(1000) overflows to inf in float math, but log(1e300) is totally fine. If you can keep computations in log-space, you often avoid overflow entirely. That’s one reason log-space is a core pattern in ML and scientific computing.\n\n### float32 vs float64 (array stacks and GPUs)\nWhen you move from math (Python floats are typically IEEE-754 double precision) to ML stacks, you may be in float32 by default. That changes the “tiny” threshold where 1 + x rounds back to 1.0 and where underflow becomes likely.\n\nMy rule of thumb:\n\n- If you’re doing probabilistic math, float64 is often worth it unless you have a strong performance reason.\n- If you’re on GPU with float16/bfloat16, assume you must use numerically stable patterns (log-sum-exp, shifted softmax, log1p/expm1 equivalents) because naive formulas will break quickly.\n\n## Practical scenarios: when to use logs (and when not to)\nLogs are a tool. Here’s how I decide when they’re the right tool.\n\n### Scenario 1: feature engineering for heavy-tailed metrics\nIf a feature is positive and spans orders of magnitude (revenue, file sizes, time-on-page, distances, counts with a long tail), a log transform often makes models behave better and plots more readable.\n\nTypical patterns I use:\n\n- Counts: log1p(count)\n- Positive continuous: log(x) (only if x > 0 is guaranteed)\n\nIf you have zeros but not negatives, log1p(x) can be a good default because it handles 0 cleanly (log1p(0) == 0).\n\n### Scenario 2: finance returns (simple returns vs log returns)\nSimple return: r = (Pt / P{t-1}) - 1\nLog return: g = log(Pt / P{t-1})\n\nI reach for log returns when I care about additivity over time (sum of log returns equals log of the total multiplicative return). And I use log1p when starting from simple returns:\n\npython\nimport math\n\ndef logreturnfromsimple(r: float) -> float:\n # Valid only for r > -1\n return math.log1p(r)\n\nprint(logreturnfromsimple(0.01))\n\n\nThis is one of those cases where log1p isn’t a micro-optimization—it’s just the correct, stable function for the job.\n\n### Scenario 3: rates and half-lives (exponential processes)\nExponential decay: N(t) = N0 exp(-k t)\nSolve for k: k = -log(N(t)/N0) / t\n\nThat formula is stable if the ratio is positive and you validate inputs. The biggest errors in practice come from domain mistakes (ratio <= 0), unit mistakes (t in days vs seconds), and confusing log base.\n\n### Scenario 4: information-like quantities (bits vs nats)\nIf you do anything related to entropy, cross-entropy, or “information” quantities, your base matters:\n\n- Natural log → units are nats\n- Log base 2 → units are bits\n\nI usually keep natural logs internally (because exp/log are natural partners), and convert to base 2 at the boundary if I need human-friendly “bits.”\n\n### Scenario 5: when I don’t use a log transform\nI avoid logs when:\n\n- Values can be negative and the sign has direct meaning (unless I’m intentionally using a signed transform like signedlog1p).\n- The metric includes zeros that mean “missing/invalid” rather than “true zero,” and the pipeline doesn’t have clear missing-value handling.\n- I need linear interpretability (e.g., “an extra unit increases outcome by X”) and multiplicative interpretation will confuse stakeholders.\n\n## Array stacks: handling invalid values without silent corruption\nOne reason logs cause production bugs is that invalid values sneak in and you end up with nan or -inf in downstream steps. Sometimes that’s fine; sometimes it silently ruins a metric.\n\n### NumPy: use errstate when you want control\nNumPy has its own behavior around warnings for invalid operations. When I’m doing log transforms on arrays, I like being explicit about what I’m okay with. Conceptually:\n\n- I decide whether log(0) should become -inf, or whether I want to mask it out.\n- I decide whether invalid values should raise, warn, or be ignored.\n\nEven if you don’t raise, you should monitor how many invalids you create (more on monitoring later).\n\n### Pandas: keep missing values as missing\nIn feature engineering, I usually prefer to turn “invalid for log” into NaN and let the imputer or model handle it, rather than forcing an arbitrary offset. That keeps the transform honest: “the log is undefined here, so I’m not inventing a number.”\n\n### Polars: choose null vs NaN intentionally\nPolars distinguishes null from NaN in a way that often plays nicely with analytics pipelines. If you’re working with expression trees, returning null for invalid log inputs can be the cleanest policy because it preserves “missingness” semantics.\n\n## Engineering patterns: “log-safe” helpers I reuse everywhere\nWhen logs show up in multiple places, I stop sprinkling inline checks and write tiny helpers. The payoff is consistency and easier testing.\n\n### A reusable validator\npython\nimport math\n\ndef isvalidforreallog(x: float) -> bool:\n return math.isfinite(x) and x > 0.0\n\n\n### A policy-driven log transform\npython\nimport math\nfrom typing import Literal\n\nInvalidPolicy = Literal[‘raise‘, ‘nan‘, ‘neginf‘]\n\ndef lnwithpolicy(x: float, , invalid: InvalidPolicy = ‘raise‘) -> float:\n if isvalidforreallog(x):\n return math.log(x)\n\n if invalid == ‘raise‘:\n raise ValueError(f‘ln undefined for x={x!r}‘)\n if invalid == ‘nan‘:\n return float(‘nan‘)\n if invalid == ‘neginf‘:\n # Useful when x represents a probability that can be exactly 0.\n return float(‘-inf‘)\n\n raise ValueError(f‘Unknown invalid policy: {invalid!r}‘)\n\n\nI like this approach because it makes “what happens at 0?” a conscious choice rather than an accident.\n\n## Performance considerations (without cargo-culting micro-optimizations)\nIn most applications, logs aren’t the bottleneck—data movement and Python overhead are. But there are a few patterns that consistently matter.\n\n### Scalar hot loops: precompute what you can\nIf you’re computing logb(x) repeatedly with a fixed base, avoid recomputing ln(base) every time. This is a tiny change but it also reads clearly:\n\npython\nimport math\n\ndef makelogbase(base: float):\n if not math.isfinite(base) or base <= 0.0 or base == 1.0:\n raise ValueError('invalid base')\n invlnbase = 1.0 / math.log(base)\n\n def f(x: float) -> float:\n return math.log(x) invlnbase\n\n return f\n\nlog5 = makelogbase(5.0)\nprint(log5(14.0))\n\n\n### Arrays: vectorize or you’ll pay for Python overhead\nIf you’re transforming a million values, don’t call math.log a million times in Python. Use vectorized operations (np.log, torch.log, jax.numpy.log, Polars expressions).\n\n### apply is the performance cliff in Pandas\nI try hard to avoid Series.apply(math.log) on large data. The vectorized path is usually both faster and less error-prone.\n\n### Choosing log2 and log10 is also a readability optimization\nPeople obsess over microbenchmarks, but clarity is the real “performance” here: fewer bugs, fewer code reviews, fewer production incidents. If you meant base 2, write math.log2.\n\n## “When should I use cmath?” (complex logs in Python)\nComplex logs are not just “logs that accept negatives.” They’re a different mathematical object with branch cuts and multiple possible values (because angles wrap). Python’s cmath.log chooses a principal value.\n\nI use cmath when:\n\n- I’m explicitly working in complex numbers already (impedance, FFT-related math, control systems).\n- The imaginary component has meaning, not just “I wanted to avoid a ValueError.”\n\nIf your input is supposed to be a positive real metric and you’re getting negatives, I treat that as a data bug, not a signal to switch to complex math.\n\n## Alternative approaches (when log is close but not quite right)\nSometimes you want the spirit of log scaling without the hard domain restriction. These are the alternatives I reach for most often.\n\n### log1p as a default for counts and sparse metrics\nIf your data includes zeros naturally (counts, clicks, sessions), log1p is a great default: it behaves like a log for large values and stays defined at zero.\n\n### Signed transforms for mixed-sign data\nI already showed signedlog1p. It’s a solid compromise when you need magnitude compression but can’t throw away negatives.\n\n### Power transforms (when you want something more flexible)\nSometimes you need a transform family that can adapt to the data distribution. In those cases, I consider power transforms (like Box-Cox / Yeo-Johnson) in ML preprocessing stacks. I don’t treat them as “better logs,” just “more knobs,” and I only use them when I’m already in a modeling context that can justify the complexity.\n\n## Monitoring and testing log-heavy code (the part people skip)\nIf logs are in a production pipeline, I want guardrails. Two lightweight practices catch most issues early.\n\n### Practice 1: count invalids and alert on drift\nIf you transform x into logx, track:\n\n- how many inputs were <= 0\n- how many were NaN/inf\n- how many outputs are NaN/inf\n\nEven a simple counter in your ETL metrics can catch upstream data regressions (like a field silently switching units or missing values being represented as 0).\n\n### Practice 2: round-trip tests for transforms\nIf you define a transform and an inverse, test the round-trip on representative values, including edge cases. For example, if you store logvalue = log(value), then exp(logvalue) should recover the original value (within float tolerance) for positive inputs.\n\npython\nimport math\n\ndef approxequal(a: float, b: float, , rel: float = 1e-12, abs: float = 0.0) -> bool:\n return abs(a - b) <= max(abs, rel * max(abs(a), abs(b)))\n\nvalues = [1e-12, 1e-6, 0.1, 1.0, 10.0, 1e6]\nfor v in values:\n recovered = math.exp(math.log(v))\n assert approxequal(v, recovered, rel=1e-12)\n\n\nI’m not trying to prove floating-point theorems here—I’m trying to catch accidental changes (like switching bases, or changing a “safe” policy from nan to -inf without realizing the downstream impact).\n\n## A quick reference: which function should I use?\nHere’s the cheat sheet I actually use when I’m moving fast.\n\n- I want the natural log of a positive scalar: math.log(x)\n- I want base 2: math.log2(x)\n- I want base 10: math.log10(x)\n- I want log(1 + x) where x might be small: math.log1p(x)\n- I want complex logs: cmath.log(x) (and I accept complex outputs)\n- I have arrays/tensors: use the array library’s log (np.log, torch.log, jax.numpy.log, Polars expressions)\n\n## Closing advice (the “boring” rules that prevent production bugs)\nIf you remember nothing else, remember this:\n\n1) Be explicit about domain. Logs need x > 0 in the real world—so decide what you do with zero, negatives, NaN, and inf.\n2) Use log1p when you see log(1 + x). It’s one of the highest-leverage numeric-stability upgrades you can make.\n3) Keep log-space values named as log-space. logprob and prob are not interchangeable.\n4) Prefer vectorized operations for arrays. Scalar math.log inside a giant loop is a performance and reliability trap.\n5) Convert bases at boundaries. Use one base internally (usually natural log) and convert for display or reporting.\n\nLogs are one of those simple functions that reward maturity: a tiny bit of discipline gives you code that’s more stable, more readable, and far harder to accidentally break.


