Package: growthbook
GrowthBook Python SDKPowerful Feature flagging and A/B testing for Python apps.Lightweight and fastLocal evaluation, no network requests requiredPython 3.6+100% test coverageFlexibletargetingUse your existing event tracking(GA, Segment, Mixpanel, custom)Remote configurationto change feature flags without deploying new codeInstallationpip install growthbook(recommended) or copygrowthbook.pyinto your projectQuick UsagefromgrowthbookimportGrowthBook# User attributes for targeting and experimentationattributes={"id":"123","customUserAttribute":"foo"}defon_experiment_viewed(experiment,result):# Use whatever event tracking system you wantanalytics.track(attributes["id"],"Experiment Viewed",{'experimentId':experiment.key,'variationId':result.variationId})# Create a GrowthBook instancegb=GrowthBook(attributes=attributes,on_experiment_viewed=on_experiment_viewed,api_host="https://cdn.growthbook.io",client_key="sdk-abc123")# Load features from the GrowthBook API with cachinggb.load_features()# Simple on/off feature gatingifgb.is_on("my-feature"):print("My feature is on!")# Get the value of a feature with a fallbackcolor=gb.get_feature_value("button-color-feature","blue")Web Frameworks (Django, Flask, etc.)For web frameworks, you should create a newGrowthBookinstance for every incoming request and calldestroy()at the end of the request to clean up resources.In Django, for example, this is best done with a simple middleware:fromgrowthbookimportGrowthBookdefgrowthbook_middleware(get_response):defmiddleware(request):request.gb=GrowthBook(# ...)request.gb.load_features()response=get_response(request)request.gb.destroy()# CleanupreturnresponsereturnmiddlewareThen, you can easily use GrowthBook in any of your views:defindex(request):feature_enabled=request.gb.is_on("my-feature")# ...Loading FeaturesThere are two ways to load feature flags into the GrowthBook SDK. You can either use the built-in fetching/caching logic or implement your own custom solution.Built-in Fetching and CachingTo use the built-in fetching and caching logic, in theGrowthBookconstructor, pass in your GrowthBookapi_hostandclient_key. If you have encryption enabled for your GrowthBook endpoint, you also need to pass thedecryption_keyinto the constructor.Then, call theload_features()method to initiate the HTTP request with a cache layer.Here's a full example:gb=GrowthBook(api_host="https://cdn.growthbook.io",client_key="sdk-abc123",# How long to cache features in seconds (Optional, default 60s)cache_ttl=60,)gb.load_features()CachingGrowthBook comes with a custom in-memory cache. If you run Python in a multi-process mode, the different processes cannot share memory, so you likely want to switch to a distributed cache system like Redis instead.Here is an example of using Redis:fromredisimportRedisimportjsonfromgrowthbookimportGrowthBook,AbstractFeatureCache,feature_repoclassRedisFeatureCache(AbstractFeatureCache):def__init__(self):self.r=Redis(host='localhost',port=6379)self.prefix="gb:"defget(self,key:str):data=self.r.get(self.prefix+key)# Data stored as a JSON string, parse into dict before returningreturnNoneifdataisNoneelsejson.loads(data)defset(self,key:str,value:dict,ttl:int)->None:self.r.set(self.prefix+key,json.dumps(value))self.r.expire(self.prefix+key,ttl)# Configure GrowthBook to use your custom cache classfeature_repo.set_cache(RedisFeatureCache())Custom ImplementationIf you prefer to handle the entire fetching/caching logic yourself, you can just pass in adictof features from the GrowthBook API directly into the constructor:# From the GrowthBook APIfeatures={'my-feature':{'defaultValue':False}}gb=GrowthBook(features=features)Note: When doing this, you do not need to specify yourapi_hostorclient_keyand you don't need to callgb.load_features().GrowthBook classThe GrowthBook constructor has the following parameters:enabled(bool) - Flag to globally disable all experiments. Default true.attributes(dict) - Dictionary of user attributes that are used for targeting and to assign variationsurl(str) - The URL of the current request (if applicable)qa_mode(boolean) - If true, random assignment is disabled and only explicitly forced variations are used.on_experiment_viewed(callable) - A function that takesexperimentandresultas arguments.api_host(str) - The GrowthBook API host to fetch feature flags from. Defaults tohttps://cdn.growthbook.ioclient_key(str) - The client key that will be passed to the API Host to fetch feature flagsdecryption_key(str) - If the GrowthBook API endpoint has encryption enabled, specify the decryption key herecache_ttl(int) - How long to cache features in-memory from the GrowthBook API (seconds, default60)features(dict) - Feature definitions from the GrowthBook API (only required ifclient_keyis not specified)forced_variations(dict) - Dictionary of forced experiment variations (used for QA)There are also getter and setter methods for features and attributes if you need to update them later in the request:gb.set_features(gb.get_features())gb.set_attributes(gb.get_attributes())AttributesYou can specify attributes about the current user and request. These are used for two things:Feature targeting (e.g. paid users get one value, free users get another)Assigning persistent variations in A/B tests (e.g. user id "123" always gets variation B)Attributes can be any JSON data type - boolean, integer, float, string, list, or dict.attributes={'id':"123",'loggedIn':True,'age':21.5,'tags':["tag1","tag2"],'account':{'age':90}}# Pass into constructorgb=GrowthBook(attributes=attributes)# Or set latergb.set_attributes(attributes)Tracking ExperimentsAny time an experiment is run to determine the value of a feature, you want to track that event in your analytics system.You can use theon_experiment_viewedoption to do this:fromgrowthbookimportGrowthBook,Experiment,Resultdefon_experiment_viewed(experiment:Experiment,result:Result):# Use whatever event tracking system you wantanalytics.track(attributes["id"],"Experiment Viewed",{'experimentId':experiment.key,'variationId':result.variationId})# Pass into constructorgb=GrowthBook(on_experiment_viewed=on_experiment_viewed)Using FeaturesThere are 3 main methods for interacting with features.gb.is_on("feature-key")returns true if the feature is ongb.is_off("feature-key")returns false if the feature is ongb.get_feature_value("feature-key", "default")returns the value of the feature with a fallbackIn addition, you can usegb.evalFeature("feature-key")to get back aFeatureResultobject with the following properties:value- The JSON-decoded value of the feature (orNoneif not defined)onandoff- The JSON-decoded value cast to booleanssource- Why the value was assigned to the user. One ofunknownFeature,defaultValue,force, orexperimentexperiment- Information about the experiment (if any) which was used to assign the value to the userexperimentResult- The result of the experiment (if any) which was used to assign the value to the userInline ExperimentsInstead of declaring all features up-front and referencing them by ids in your code, you can also just run an experiment directly. This is done with therunmethod:fromgrowthbookimportExperimentexp=Experiment(key="my-experiment",variations=["red","blue","green"])# Either "red", "blue", or "green"print(gb.run(exp).value)As you can see, there are 2 required parameters for experiments, a string key, and an array of variations. Variations can be any data type, not just strings.There are a number of additional settings to control the experiment behavior:key(str) - The globally unique tracking key for the experimentvariations(any[]) - The different variations to choose betweenseed(str) - Added to the user id when hashing to determine a variation. Defaults to the experimentkeyweights(float[]) - How to weight traffic between variations. Must add to 1.coverage(float) - What percent of users should be included in the experiment (between 0 and 1, inclusive)condition(dict) - Targeting conditionsforce(int) - All users included in the experiment will be forced into the specified variation indexhashAttribute(string) - What user attribute should be used to assign variations (defaults to "id")hashVersion(int) - What version of our hashing algorithm to use. We recommend using the latest version2.namespace(tuple[str,float,float]) - Used to run mutually exclusive experiments.Here's an example that uses all of them:exp=Experiment(key="my-test",# Variations can be a list of any data typevariations=[0,1],# If this changes, it will re-randomize all users in the experimentseed="abcdef123456",# Run a 40/60 experiment instead of the default even split (50/50)weights=[0.4,0.6],# Only include 20% of users in the experimentcoverage=0.2,# Targeting condition using a MongoDB-like syntaxcondition={'country':'US','browser':{'$in':['chrome','firefox']}},# Use an alternate attribute for assigning variations (default is 'id')hashAttribute="sessionId",# Use the latest hashing algorithmhashVersion=2,# Includes the first 50% of users in the "pricing" namespace# Another experiment with a non-overlapping range will be mutually exclusive (e.g. [0.5, 1])namespace=("pricing",0,0.5),)Inline Experiment Return ValueA call torunreturns aResultobject with a few useful properties:result=gb.run(exp)# If user is part of the experimentprint(result.inExperiment)# True or False# The index of the assigned variationprint(result.variationId)# e.g. 0 or 1# The value of the assigned variationprint(result.value)# e.g. "A" or "B"# If the variation was randomly assigned by hashing user attributesprint(result.hashUsed)# True or False# The user attribute used to assign a variationprint(result.hashAttribute)# "id"# The value of that attributeprint(result.hashValue)# e.g. "123"TheinExperimentflag will be false if the user was excluded from being part of the experiment for any reason (e.g. failed targeting conditions).ThehashUsedflag will only be true if the user was randomly assigned a variation. If the user was forced into a specific variation instead, this flag will be false.Example Experiments3-way experiment with uneven variation weights:gb.run(Experiment(key="3-way-uneven",variations=["A","B","C"],weights=[0.5,0.25,0.25]))Slow rollout (10% of users who match the targeting condition):# User is marked as being in "qa" and "beta"gb=GrowthBook(attributes={"id":"123","beta":True,"qa":True,},)gb.run(Experiment(key="slow-rollout",variations=["A","B"],coverage=0.1,condition={'beta':True}))Complex variationsresult=gb.run(Experiment(key="complex-variations",variations=[("blue","large"),("green","small")],))# Either "blue,large" OR "green,small"print(result.value[0]+","+result.value[1])Assign variations based on something other than user idgb=GrowthBook(attributes={"id":"123","company":"growthbook"})# Users in the same company will always get the same variationgb.run(Experiment(key="by-company-id",variations=["A","B"],hashAttribute="company"))LoggingThe GrowthBook SDK uses a Python logger with the namegrowthbookand includes helpful info for debugging as well as warnings/errors if something is misconfigured.Here's an example of logging to the consoleimportlogginglogger=logging.getLogger('growthbook')logger.setLevel(logging.DEBUG)handler=logging.StreamHandler()formatter=logging.Formatter('%(asctime)s%(name)s%(levelname)s%(message)s')handler.setFormatter(formatter)logger.addHandler(handler)