This module has an unusually large API surface, so the symbol listing in doc(fastcore.basics) is suppressed via __pyskill_sigs__: the exported cells below assemble into a curated module docstring instead.
Basics
The staples used throughout fastai code, each replacing a common multi-line pattern with a one-liner: ifnone(a,b) is b if a is None else a (though both args are always evaluated); listify and tuplify convert anything to a list or tuple the way you’d mean it (None becomes [], a str or dict stays a single item, a generator is consumed); and basic_repr('a,b') gives a class a deterministic key=value repr with no memory address, so notebook and git diffs stay clean.
Since b if a is None else a is such a common pattern, we wrap it in a function. However, be careful, because python will evaluate botha and b when calling ifnone (which it doesn’t do if using the if version directly).
The complement of ifnone for the cases where None is not an acceptable answer: many lookups signal “not found” with None (first on an empty sequence, dict.get, a C API returning a null handle), and the caller’s next line is often if x is None: raise .... req folds that guard into the lookup expression itself.
In types which provide rich display functionality in Jupyter, their __repr__ is also called in order to provide a fallback text representation. Unfortunately, this includes a memory address which changes on every invocation, making it non-deterministic. This causes diffs to get messy and creates conflicts in git. To fix this, put __repr__=basic_repr() inside your class.
class SomeClass: __repr__=basic_repr()repr(SomeClass())
'SomeClass()'
If you pass a list of attributes (flds) of an object, then this will generate a string with the name of each attribute and its corresponding value. The format of this string is key=value, where key is the name of the attribute, and value is the value of the attribute. For each value, attempt to use the __name__ attribute, otherwise fall back to using the value’s __repr__ when constructing the string.
class SomeClass: a=1 b='foo'__repr__=basic_repr('a,b')repr(SomeClass())
"SomeClass(a=1, b='foo')"
Nested objects work too:
class AnotherClass: c=SomeClass() d='bar'__repr__=basic_repr(['c', 'd'])repr(AnotherClass())
Most often you’ll want to call mk_class, since it adds the class to your module. See mk_class for more details and examples of use (which also apply to get_class).
A __init__ is provided that sets attrs for any kwargs, and for any args (matching by position to fields), along with a __repr__ which prints all attrs. The docstring is set to doc. You can pass funcs which will be added as attrs with the function names.
test_eq([o for i,o inzip(range(5), Inf.count)], [0, 1, 2, 3, 4])test_eq([o for i,o inzip(range(5), Inf.zeros)], [0]*5)test_eq([o for i,o inzip(range(5), Inf.ones)], [1]*5)test_eq([o for i,o inzip(range(5), Inf.nones)], [None]*5)
Operator Functions
Curried versions of the comparison and arithmetic functions in Python’s operator module: lt gt le ge eq ne add sub mul truediv is_ is_not in_ mod. With two args they work like operator’s versions; with one arg they return a partial that binds it as the second argument, so lt(3) means “is less than 3” and in_(vals) means “is contained in vals”. They read especially well as cmp arguments to fastcore.test.test, e.g. test(x, valid, in_).
# test if element is in anotherassert in_('c', ('b', 'c', 'a'))assert in_(4, [2,3,4,5])assert in_('t', 'fastai')assertnot in_('h', 'fastai')# use in_ as a partialassert in_('fastai')('t')assert in_([2,3,4,5])(4)assertnot in_('fastai')('h')
In addition to in_, the following functions are provided matching the behavior of the equivalent versions in operator: lt gt le ge eq ne add sub mul truediv is_ is_not mod.
Similarly to in_, they also have additional functionality: if you only pass one param, they return a partial function that passes that param as the second positional parameter.
AttrDict is a dict whose keys are also attributes, so d.foo reads and writes d['foo'] (to convert a whole nested structure at once, see dict2obj in fastcore.xtras); NS is the same idea built on SimpleNamespace, adding indexing and iteration. store_attr(), called inside __init__, stores the function’s arguments as attributes of self in one line:
These functions reduce boilerplate when setting or manipulating attributes or properties of objects.
SimpleNamespace subclass that also adds iter and dict support
This is very similar to AttrDict, but since it starts with SimpleNamespace, it has some differences in behavior. You can use it just like SimpleNamespace:
Backport of py3.10 get_annotations that returns globals/locals
In Python 3.10 inspect.get_annotations was added. However previous versions of Python are unable to evaluate type annotations correctly if from future import __annotations__ is used. Furthermore, all annotations are evaluated, even if only some subset are needed. get_annotations_ex provides the same functionality as inspect.get_annotations, but works on earlier versions of Python, and returns the globals and locals needed to evaluate types.
Like typing.get_type_hints but returns {} if not allowed type
def type_hints(f):"Like `typing.get_type_hints` but returns `{}` if not allowed type"ifnotisinstance(f, _allowed_types): return {} ann,glb,loc = get_annotations_ex(f)return {k:_eval_type(v,glb,loc) for k,v in ann.items()}
For example, type func is allowed so type_hints returns the same value as typing.get_hints:
def f(a:int)->bool: ... # a function with type hints (allowed)exp = {'a':int,'return':bool}test_eq(type_hints(f), typing.get_type_hints(f))test_eq(type_hints(f), exp)
However, class is not an allowed type, so type_hints returns {}:
class _T:def__init__(self, a:int=0)->bool: ...assertnot type_hints(_T)
Like inspect.signature, but a declared Parameter.empty default is kept as a default
inspect uses Parameter.empty both as its “no default” marker and as a value a function may declare as an actual default (a common sentinel idiom, e.g. fasthtml.core.add_sig_param). Plain signature() reads such a declaration back as “no default”, producing a signature that fails validation as soon as it is rebuilt, e.g. by Signature.replace. signature_ex keeps the declared default distinct, so the signature stays valid and renders as the source declared it:
Change attrs in cls with names in ps to properties
class T:def a(self): return1def b(self): return2properties(T,'a')test_eq(T().a,1)test_eq(T().b(),2)
The case-convention functions below share one splitter. id_words segments an identifier into its words and never changes case: punctuation characters in splits are treated as separators, and the letter c enables splitting at case boundaries, including after acronym runs. Constrain splits when a convention gives characters different meanings, such as CSS pseudo-selectors where : must not split. Separator runs come back as empty words, so names like __init__ survive a round trip.
Split identifier s into words: punctuation chars in splits are separators (runs kept as empty words, so they survive a round trip), ‘c’ splits case boundaries
Each to_* emitter owns its casing policy: to_camel keeps acronym words intact (so an already-camel name round-trips exactly, which HtmxOn-style consumers rely on), to_pascal Title-cases every word (the historic snake2camel output), and to_kebab/to_snake lowercase throughout.
Property decorator with dependency update triggering
DepProp is a descriptor that stores a value and calls a “change” function whenever that value changes or is deleted. This is useful for invalidating caches or triggering side effects when a dependency is updated. An optional normalizer can preprocess values before storage.
Inherit from this to have all attr accesses in self._xtra passed down to self.default
Inherit from GetAttr to have attr access passed down to an instance attribute. This makes it easy to create composites that don’t require callers to know about their components. For a more detailed discussion of how this works as well as relevant context, we suggest reading the delegated composition section of this blog article.
You can customise the behaviour of GetAttr in subclasses via; - _default - By default, this is set to 'default', so attr access is passed down to self.default - _default can be set to the name of any instance attribute that does not start with dunder __ - _xtra - By default, this is None, so all attr access is passed down - You can limit which attrs get passed down by setting _xtra to a list of attribute names
To illuminate the utility of GetAttr, suppose we have the following two classes, _WebPage which is a superclass of _ProductPage, which we wish to compose like so:
How do we make it so we can just write p.author, instead of p.page.author to access the author attribute? We can use GetAttr, of course! First, we subclass GetAttr when defining _ProductPage. Next, we set self.default to the object whose attributes we want to be able to access directly, which in this case is the page argument passed on initialization:
class _ProductPage(GetAttr):def__init__(self, page, price): self.default,self.price = page,price #self.default allows you to access page directly.p = _ProductPage(page, 15.0)
Now, we can access the author attribute directly from the instance:
test_eq(p.author, 'Sylvain')
If you wish to store the object you are composing in an attribute other than self.default, you can set the class attribute _data as shown below. This is useful in the case where you might have a name collision with self.default:
class _C(GetAttr): _default ='_data'# use different component name; `self._data` rather than `self.default`def__init__(self,a): self._data = adef foo(self): noopt = _C('Hi')test_eq(t._data, 'Hi') with expect_fail(): t.default # we no longer have self.defaulttest_eq(t.lower(), 'hi')test_eq(t.upper(), 'HI')assert'lower'indir(t)assert'upper'indir(t)
By default, all attributes and methods of the object you are composing are retained. In the below example, we compose a str object with the class _C. This allows us to directly call string methods on instances of class _C, such as str.lower() or str.upper():
class _C(GetAttr):# allow all attributes and methods to get passed to `self.default` (by leaving _xtra=None)def__init__(self,a): self.default = adef foo(self): noopt = _C('Hi')test_eq(t.lower(), 'hi')test_eq(t.upper(), 'HI')assert'lower'indir(t)assert'upper'indir(t)
However, you can choose which attributes or methods to retain by defining a class attribute _xtra, which is a list of allowed attribute and method names to delegate. In the below example, we only delegate the lower method from the composed str object when defining class _C:
class _C(GetAttr): _xtra = ['lower'] # specify which attributes get passed to `self.default`def__init__(self,a): self.default = adef foo(self): noopt = _C('Hi')test_eq(t.default, 'Hi')test_eq(t.lower(), 'hi')with expect_fail(): t.upper() # upper wasn't in _xtra, so it isn't available to be calledassert'lower'indir(t)assert'upper'notindir(t)
You must be careful to properly set an instance attribute in __init__ that corresponds to the class attribute _default. The below example sets the class attribute _default to data, but erroneously fails to define self.data (and instead defines self.default).
Failing to properly set instance attributes leads to errors when you try to access methods directly:
class _C(GetAttr): _default ='data'# use a bad component name; i.e. self.data does not existdef__init__(self,a): self.default = adef foo(self): noop# TODO: should we raise an error when we create a new instance ...t = _C('Hi')test_eq(t.default, 'Hi')# ... or is it enough for all GetAttr features to raise errorswith expect_fail(): t.datawith expect_fail(): t.lower()with expect_fail(): t.upper()with expect_fail(): dir(t)
Use in __getattr__ to delegate to attr to without inheriting from GetAttr
delegate_attr is a functional way to delegate attributes, and is an alternative to GetAttr. We recommend reading the documentation of GetAttr for more details around delegation.
You can use achieve delegation when you define __getattr__ by using delegate_attr:
class _C:def__init__(self, o): self.o = o # self.o corresponds to the `to` argument in delegate_attr.def__getattr__(self, k): return delegate_attr(self, k, to='o')t = _C('HELLO') # delegates to a stringtest_eq(t.lower(), 'hello')t = _C(np.array([5,4,3])) # delegates to a numpy arraytest_eq(t.sum(), 12)t = _C(pd.DataFrame({'a': [1,2], 'b': [3,4]})) # delegates to a pandas.DataFrametest_eq(t.b.max(), 4)
Extensible Types
ShowPrint is a base class that defines a show method, which is used primarily for callbacks in fastai that expect this method to be defined.
Int, Float, and Str extend int, float and str respectively by adding an additional show method by inheriting from ShowPrint.
Tools for making and transforming functions: compose(f,g,...) chains functions left to right; bind is partial extended with arg0,arg1,… placeholders for reordering positional arguments; and fail_clean re-raises exceptions with the library’s own traceback frames stripped, for errors that are part of a function’s contract rather than bugs.
Utilities for functional programming or for defining, modifying, or debugging functions.
Same as partial, except you can use arg0arg1 etc param placeholders
bind is the same as partial, but also allows you to reorder positional arguments using variable name(s) arg{i} where i refers to the zero-indexed positional argument. bind as implemented currently only supports reordering of up to the first 5 positional arguments.
Consider the function myfunc below, which has 3 positional arguments. These arguments can be referenced as arg0, arg1, and arg1, respectively.
def myfn(a,b,c,d=1,e=2): return(a,b,c,d,e)
In the below example we bind the positional arguments of myfn as follows:
The second input 14, referenced by arg1, is substituted for the first positional argument.
We supply a default value of 17 for the second positional argument.
The first input 19, referenced by arg0, is subsituted for the third positional argument.
This is an example of using bind like partial and do not reorder any arguments:
test_eq(bind(myfn)(17,19,14), (17,19,14,1,2))
bind can also be used to change default values. In the below example, we use the first input 3 to override the default value of the named argument e, and supply default values for the first three positional arguments:
Every arg{i} placeholder refers to the i-th call-time argument, in positional and keyword bindings alike; arguments past the highest placeholder index are appended:
class partial0:"Like `partialler`, but args passed to callable are inserted at started, instead of at end"def__init__(self, f, *args, order=None, **kwargs):self.f,self.args,self.kwargs = f,args,kwargsself.order = ifnone(order, getattr(f,'order',None))self.__doc__ = f.__doc__def__call__(self, *args, **kwargs): returnself.f(*args, *self.args, **kwargs, **self.kwargs)
f = partial0(_f, 2)test_eq(f.order, 1)test_eq(f(3), 1) # NB: different to `partialler` example
Re-raise excs (default: Exception) without internal traceback frames
fail_clean marks a boundary where errors are part of a function’s contract rather than something to debug: exceptions of the listed types (any Exception by default) are re-raised with the internal traceback frames removed, so the caller sees the message and their own call site instead of the library’s plumbing. The original exception object is preserved, including its attributes. Exceptions not listed keep their full stack for debugging. Use @fail_clean bare, or pass exception types like @fail_clean(ValueError):
Wrap f to accept a single dict, unpacking it as keyword args
dstar wraps a function to accept a single dictionary, unpacking it as keyword arguments (the ** counterpart of star in fastcore.foundation). For instance:
A more realistic example showing API request configuration. Each request dictionary may have different keys present, and dspread handles this naturally (missing keys use the function’s defaults):
~Self is a concise alternative to lambda for a function that operates on a single object (note the capitalization!). Write the chain of attribute accesses, method calls, and indexing just as you would after a variable name, with ~Self in its place, and the result is a plain function that runs the chain on its argument:
~Self.sum() is lambda o: o.sum()
~Self.imag is lambda o: o.imag
~Self[1] is lambda o: o[1]
~Self.sum().real is lambda o: o.sum().real
~Self alone is the identity, lambda o: o
Since ., (), and [] bind tighter than ~, the whole chain builds first and ~ then converts it to a function, so the chain never needs its own parentheses: map(~Self.imag, nums) works as written.
The functions ~Self returns are ordinary functions: safe to introspect, and independent of any later chains built from the same prefix. Parens mean what they mean in plain Python: a step written with () is called, so a chain that calls a non-callable attribute raises TypeError rather than quietly fetching it:
test_eq((~Self.imag)(3), 0)with expect_fail(TypeError, 'not callable'): (~Self.imag())(3) # `imag` is an attribute; `()` means call, as in plain Pythontest_eq(list(map(~Self.real, [3,4])), [3,4])x = np.array([3.,1])test_eq((~Self.sum().real)(x), 4.)test_eq((~Self)(x) is x, True)test_eq((~Self[1])(x), 1)test_eq((~Self.strip()[0])(' abc '), 'a')g1 = Self.strip()g2 = g1.upper()test_eq((~g1)(' hi '), 'hi')test_eq((~g2)(' hi '), 'HI')assertnothasattr(Self.sum(), '__wrapped__')with expect_fail(TypeError, 'applied'): Self.strip()()with expect_fail(TypeError, '~Self'): bool(Self.imag)
Calling Self directly adds a call step to the chain, so ~Self(1,b=2) is a function which calls whatever it’s applied to with those arguments:
@patch adds a function to an existing class as a method, using the function’s self: type annotation to pick the class (a union annotation patches several classes at once); @patch_to(Cls) is the same with the class passed explicitly. Both take as_prop, set_prop, and cls_method. fastai code uses this to build classes incrementally across a notebook, so expect to find a class’s methods defined far from the class itself:
Copy a non-builtin function (NB copy.copy does not work for this)
Sometimes it may be desirable to make a copy of a function that doesn’t point to the original object. When you use Python’s built in copy.copy or copy.deepcopy to copy a function, you get a reference to the original object:
import copy as cp
def foo(): passa = cp.copy(foo)b = cp.deepcopy(foo)a.someattr ='hello'# since a and b point at the same object, updating a will update btest_eq(b.someattr, 'hello')assert a is foo and b is foo
However, with copy_func, you can retrieve a copy of a function without a reference to the original object:
c = copy_func(foo) # c is an indpendent objectassert c isnot foo
The @patch_to decorator allows you to monkey patch a function into a class as a method:
class _T3(int): pass@patch_to(_T3)def func1(self, a): returnself+at = _T3(1) # we initialized `t` to a type int = 1test_eq(t.func1(2), 3) # we add 2 to `t`, so 2 + 1 = 3if sys.version_info >= (3,11): test_eq(_T3.func1.__code__.co_qualname, '_T3.func1')
You can access instance properties in the usual way via self:
class _T4():def__init__(self, g): self.g = g@patch_to(_T4)def greet(self, x): returnself.g + xt = _T4('hello ') # this sets self.g = 'hello 'test_eq(t.greet('world'), 'hello world') #t.greet('world') will append 'world' to 'hello '
You can instead specify that the method should be a class method by setting cls_method=True:
class _T5(int): attr =3# attr is a class attribute we will access in a later method@patch_to(_T5, cls_method=True)def func(cls, x): return cls.attr + x # you can access class attributes in the normal waytest_eq(_T5.func(4), 7)
Additionally you can specify that the function you want to patch should be a class attribute with as_prop=True:
Instead of passing one class to the @patch_to decorator, you can pass multiple classes in a tuple to simulteanously patch more than one class with the same method:
@patch is an alternative to @patch_to that allows you similarly monkey patch class(es) by using type annotations:
class _T8(int): pass@patchdef func(self:_T8, a): returnself+at = _T8(1) # we initilized `t` to a type int = 1test_eq(t.func(3), 4) # we add 3 to `t`, so 3 + 1 = 4test_eq(t.func.__qualname__, '_T8.func')if sys.version_info >= (3,11): test_eq(_T8.func.__code__.co_qualname, '_T8.func')
class MyMath: pass@patch_to(MyMath, static_method=True)def add(a, b): return a + b@patch(static_method=True)def mul(a:MyMath, b): return a * btest_eq(MyMath.add(2, 3), 5)test_eq(MyMath.mul(2, 3), 6)
Similarly to patch_to, you can supply a union of classes instead of a single class in your type annotations to patch multiple classes:
class _T9(int): pass@patchdef func2(x:_T8|_T9, a): return x*a # will patch both _T8 and _T9t = _T8(2)test_eq(t.func2(4), 8)test_eq(t.func2.__qualname__, '_T8.func2')t = _T9(2)test_eq(t.func2(4), 8)test_eq(t.func2.__qualname__, '_T9.func2')
Just like patch_to decorator you can use as_prop, set_prop, and cls_method parameters with patch decorator:
class _T5(int): attr =3# attr is a class attribute we will access in a later method@patch(cls_method=True)def func(cls:_T5, x): return cls.attr + x # you can access class attributes in the normal waytest_eq(_T5.func(4), 7)
class Color(Enum): RED =1 GREEN =2@patch(cls_method=True)def from_name(cls: Color, s): return cls[s.upper()]test_eq(Color.from_name.__name__, 'from_name')test_eq(Color.from_name('red'), Color.RED)test_eq(Color.from_name('GREEN'), Color.GREEN)
def extend_enum( cls, # Enum class to modify n, # Name of the new enum member v, # Value of the new enum member):
Add new member n with value v to enum class cls at runtime
extend_enum mutates an existing enum class by constructing a new member, registering it in the enum’s internal lookup tables, and attaching it as a class attribute, so it behaves like a normal enum member created in the original class definition.
from enum import Enum
class Color(Enum): red =1; blue =2extend_enum(Color, 'green', 3)Color.green, Color['green'], Color(3)
A base class/mixin for objects that should not serialize all their state
class _T(Stateful):def__init__(self):super().__init__()self.a=1self._state['test']=2t = _T()t2 = pickle.loads(pickle.dumps(t))test_eq(t.a,1)test_eq(t._state['test'],2)test_eq(t2.a,1)test_eq(t2._state,{})
Override _init_state to do any necessary setup steps that are required during __init__ or during deserialization (e.g. pickle.load). Here’s an example of how Stateful simplifies the official Python example for Handling Stateful Objects.
class TextReader(Stateful):"""Print and number lines in a text file.""" _stateattrs=('file',)def__init__(self, filename):self.filename,self.lineno = filename,0super().__init__()def readline(self):self.lineno +=1 line =self.file.readline()if line: returnf"{self.lineno}: {line.strip()}"def _init_state(self):self.file=open(self.filename)for _ inrange(self.lineno): self.file.readline()
Case-insensitive convert string s too a bool (y,yes,t,true,on,1->True)
True values are ‘y’, ‘yes’, ‘t’, ‘true’, ‘on’, and ‘1’; false values are ‘n’, ‘no’, ‘f’, ‘false’, ‘off’, and ‘0’. Raises ValueError if ‘val’ is anything else.
for o in"y YES t True on 1".split(): assert str2bool(o)for o in"n no FALSE off 0".split(): assertnot str2bool(o)for o in0,None,'',False: assertnot str2bool(o)for o in1,True: assert str2bool(o)
You can have automatic casting based on heuristics by specifying typed(cast=True). If casting is not possible, a TypeError is raised.
@typed(cast=True)def discount(price:int, pct:float) ->float:return (1-pct) * priceassert90.0== discount(100.5, .1) # will auto cast 100.5 to the int 100assert90.0== discount(' 100 ', .1) # will auto cast the str "100" to the int 100with ExceptionExpected(TypeError): discount("a", .1)
We can also optionally allow multiple types by enumarating the types in a tuple as illustrated below:
sig_with_params lets you modify a function signature by adding, replacing, or removing parameters. This is useful when creating wrapper functions or decorators that need to adjust the signature of the wrapped function.
The result is kind-sorted (kindsort: a stable sort on Parameter.kind, Python’s own ordering rule), so composing groups of params can never produce an illegal signature: a collector in sig stays last however many params are added after it:
def bar(a, *, b=2, **kwargs): passnew_sig = sig_with_params(signature(bar), c=Parameter('c', Parameter.KEYWORD_ONLY, default=3))test_eq(str(new_sig), '(a, *, b=2, c=3, **kwargs)')test_eq([p.name for p in kindsort(new_sig.parameters.values())], ['a', 'b', 'c', 'kwargs']) # already legal: unchanged
Python’s json.dumps can handle custom objects through a default callback, but it does not look for serialization methods on the objects themselves. Therefore even a class is not normally serializable:
class Person:def__init__(self, name): self.name = namedef __json__(self): returndict(name=self.name)with expect_fail(TypeError, 'Person is not JSON serializable'): json.dumps(Person('Alyssa'))
xdumps recognizes __json__ and serializes the value it returns. The protocol is applied recursively, so custom objects can appear inside ordinary lists and dictionaries:
people =dict(team=[Person('Alyssa'), Person('Ben')])test_eq(json.loads(xdumps(people)), dict(team=[dict(name='Alyssa'), dict(name='Ben')]))
Some standard library types cannot implement __json__, since builtin types cannot gain methods, so xdumps handles the common ones directly: datetimes, dates and times become ISO8601 strings, UUID and Path become strings, and enums become their values. An object’s own __json__ still comes first, and default remains the escape hatch for anything else:
class Priority(enum.Enum): high =1d =dict(t=datetime(2026,8,2,10,30), day=date(2026,8,2), u=UUID(int=18), p=Path('a/b'), pri=Priority.high)test_eq(json.loads(xdumps(d)), dict(t='2026-08-02T10:30:00', day='2026-08-02', u='00000000-0000-0000-0000-000000000012', p='a/b', pri=1))
revive_dates is the inverse for the datetime case: it walks a decoded JSON tree and converts any string in ISO8601 form (Z suffix included) back into a datetime. Conversion is by shape, so a string that merely looks like a timestamp converts too - apply it to trees where date strings can only be dates, such as message headers, rather than to arbitrary user content:
Recursively convert ISO8601-formatted strings in a decoded JSON tree into datetime objects
msg = json.loads(xdumps(dict(date=datetime(2026,8,2,10,30), note='sent on 2026-08-02, not a timestamp')))test_eq(revive_dates(dict(d='2026-08-02T10:30:00Z'))['d'], datetime.fromisoformat('2026-08-02T10:30:00+00:00'))revive_dates(msg)
Notebook functions
ipython_shell
def ipython_shell():
Same as get_ipython but returns False if not in IPython
in_ipython
def in_ipython():
Check if code is running in some kind of IPython environment
in_colab
def in_colab():
Check if the code is running in Google Colaboratory
in_jupyter
def in_jupyter():
Check if the code is running in a jupyter notebook
in_notebook
def in_notebook():
Check if the code is running in a jupyter notebook
These variables are available as booleans in fastcore.basics as IN_IPYTHON, IN_JUPYTER, IN_COLAB and IN_NOTEBOOK.